Compare commits
64
Commits
a1687486bc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
342c612358 | ||
|
|
27a3750e36 | ||
|
|
38cc9d4acb | ||
|
|
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 |
No files matched your search
+25
@@ -4,7 +4,20 @@
|
||||
|
||||
# ---- 宿主测试二进制(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
|
||||
@@ -31,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
|
||||
|
||||
@@ -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,6 +52,11 @@ 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 能力速览
|
||||
@@ -65,6 +76,8 @@ cd ../examples/hello
|
||||
- **图标库 `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,失败静默)。
|
||||
|
||||
@@ -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 全系统仿真
|
||||
|
||||
@@ -1388,6 +1388,37 @@ void meow_scroll_set_friction(meow_scroll *s, int tau_ms); /* 默认 180ms
|
||||
- **机制层新增**:`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 追加)
|
||||
|
||||
@@ -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,318 @@
|
||||
# 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` 用于宿主/真机一眼验收。
|
||||
6. **M16 续修同源入口**(同一 TU,`#if MEOW_TIME32_KERNEL_ABI` 限定 mips o32):
|
||||
`__timedwait_cp`/`__timedwait`(futex 相对超时)、`__timerfd_settime64`/
|
||||
`__timerfd_gettime64`/`timerfd_create`、`__clock_settime64`。换算逻辑抽成平台无关的
|
||||
`meow_time32_{rel,abs}_from_parts()` / itimerspec 转换(`meow_ui_internal.h`),
|
||||
宿主 `test_meow_time64.c` 直接覆盖。清单、逐项证据与未修项配方见 §6/§7。
|
||||
|
||||
非 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()` 的修正也在同一份归档里生效(链接期覆盖)。
|
||||
- **同步等待(M16)**:`pthread_cond_timedwait` / `sem_timedwait` / `pthread_join` 超时 /
|
||||
`aio_suspend` / 非 PI 的 `pthread_mutex_timedlock`/rwlock timed 的**相对超时按真实剩余
|
||||
时间**生效(不再「亚秒即超时」)。SDK 侧直接受益者是 `meow_ui_audio.c` 的 5 处条件等待
|
||||
(:312/:351/:417/:431/:499)。
|
||||
`timerfd_*`、`clock_settime` 一并修正(当前无调用方)。
|
||||
- 真机写入红线未触碰;本单只改 SDK 源与文档/测试。
|
||||
|
||||
## 6. 同源入口全量清点(M16)
|
||||
|
||||
M16 把 zig 0.17 musl 源里所有 `#if defined(SYS_*_time64) && !defined(__ILP32__)` 守卫
|
||||
逐条编译、反汇编核对(`zig cc --target=mipsel-linux-musleabi`),结论如下。
|
||||
「回退分支」= `__ILP32__` 被定义时实际编译进去的代码。
|
||||
|
||||
### 6.1 受损(回退分支把 time64 结构直传 time32 系统调用)
|
||||
|
||||
| musl 源(守卫行) | 入口 | 回退分支实际行为 | 状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| `thread/__timedwait.c:14` | futex 相对超时 → `pthread_cond_timedwait` / `sem_timedwait` / `pthread_join` 超时 / `aio_suspend` / 非 PI `pthread_mutex_timedlock` / rwlock timed | `__syscall_cp(SYS_futex=4238, …, to)`,`to` 是 16B time64 结构;内核只读前 8B:`tv_nsec` 落在 `tv_sec` 高半字(恒 0)→ 亚秒等待变 0(立即 ETIMEDOUT)、整秒等待丢纳秒 | **M16 已接管** |
|
||||
| `linux/timerfd.c:14` | `timerfd_settime` | 32B itimerspec 直传 `SYS_timerfd_settime32=4323`;内核按 16B 读 → `value.tv_sec` 读到调用者的 `interval.tv_nsec`,`value.tv_nsec` 读到填充字节 | **M16 已接管** |
|
||||
| `linux/timerfd.c:42` | `timerfd_gettime` | 4322 只写 16B,调用者按 32B 解 → 字段串位 | **M16 已接管** |
|
||||
| `time/clock_settime.c:9` | `clock_settime` | 16B 直传 `SYS_clock_settime32=4262` → nsec 恒 0 | **M16 已接管** |
|
||||
| `signal/sigtimedwait.c:10` | `sigtimedwait` | 16B 直传 `SYS_rt_sigtimedwait=4197` | 未修(SDK/App 未用,§7.3 配方) |
|
||||
| `ipc/semtimedop.c:19` | `semtimedop` | 16B 直传 `SYS_ipc=4117`(`IPCOP_semtimedop`) | 未修(未用) |
|
||||
| `time/timer_settime.c:13` | `timer_settime` | 32B itimerspec 直传 4258 | 未修(未用) |
|
||||
| `time/timer_gettime.c:11` | `timer_gettime` | 4259 只写 16B | 未修(未用) |
|
||||
| `stat/utimensat.c:15` | `utimensat`(`futimens` 同源) | `times[2]`(32B)直传 4316 | 未修(仅 ncm 宿主 bench/测试出现) |
|
||||
| `network/recvmmsg.c:21` | `recvmmsg` | 16B timeout 直传 4335 | 未修(未用) |
|
||||
| `mq/mq_timedsend.c:10` | `mq_timedsend` | 16B 直传 4273 | 未修(未用) |
|
||||
| `thread/pthread_mutex_timedlock.c:8` | PI 互斥量的 timedlock 快路径 | 16B 直传 4238(`FUTEX_LOCK_PI`) | 未修(非 PI 走已修的 `__timedwait`;SDK/App 不用 PI 互斥量) |
|
||||
|
||||
唯一在用的受害者是 futex:`meow_ui_audio.c`(5 处,:312/:351/:417/:431/:499)、
|
||||
`meow_audiod`、ncm 播放线程都用
|
||||
`pthread_cond_timedwait`,修复前「剩 0.4s」会被内核当成 `{0,0}` 立即超时 → 音频环形缓冲
|
||||
的等待退化成忙轮询。
|
||||
|
||||
### 6.2 不受影响(回退分支自己做了宽度转换)——纠正 M11 §6 的猜测
|
||||
|
||||
| musl 源 | 证据 |
|
||||
| --- | --- |
|
||||
| `select/pselect.c:15` | 回退分支 `syscall_cp(SYS_pselect6=4301, …, (long[]){s, ns}, …)`:o32 的 `long`=4B → 正好 8B time32 |
|
||||
| `select/ppoll.c:14` | 同上(4302)。反汇编 `__ppoll_time64`:`lw 0x0/0x8` 读 16B 结构 → `sw` 成栈上两字 → `addiu $4,$zero,0x10ce`(4302) |
|
||||
| `select/select.c:28` | mips 无 `SYS_select`,走 `(long[]){s,ns}` + 4301;同 pselect |
|
||||
| `linux/clock_adjtime.c:40` | 回退分支保留 `if (sizeof(time_t) > sizeof(long))`,构造 long 版 `struct ktimex` |
|
||||
| `misc/getrusage.c:9` | 同上宽度分支(`dest = &ru_maxrss - 4*sizeof(long)` + 回填) |
|
||||
| `linux/wait4.c:11` | 同上宽度分支 |
|
||||
|
||||
M11 §6 把 pselect/ppoll 列为「未验证」是保守估计;本次逐条反汇编确认它们**本来就是对的**,
|
||||
不需要接管(也就没有可修的低风险项)。
|
||||
|
||||
## 7. 接管机制与未修项配方(M16)
|
||||
|
||||
### 7.1 符号名不是 API 名:`_REDIR_TIME64` 把调用点改了名
|
||||
|
||||
zig 的 `alltypes.h` 无条件 `_REDIR_TIME64=1`,头文件里每个受影响 API 都有一行
|
||||
`__REDIR(x, __x_time64)`(`generic-musl/*.h`)。应用里写 `timerfd_settime(...)`,编译出来
|
||||
引用的是 `__timerfd_settime64`。所以接管必须定义**重定向后的符号**:
|
||||
|
||||
| API | 应用引用的符号 | libc 该 `.o` 还定义(必须一并接管,否则重复定义) |
|
||||
| --- | --- | --- |
|
||||
| `timerfd_settime/gettime` | `__timerfd_settime64` / `__timerfd_gettime64` | `timerfd_create` |
|
||||
| `clock_settime` | `__clock_settime64` | (无) |
|
||||
| 内部 `__timedwait_cp` | `__timedwait_cp` | `__timedwait`、`__eintr_valid_flag`(弱) |
|
||||
|
||||
只定义 `__timedwait_cp` 是**不行**的:libc 的 `__timedwait.o` 会因 `__timedwait` 未解析被
|
||||
拉入,与本文的重定义撞车。实测:
|
||||
|
||||
```
|
||||
ld.lld: error: duplicate symbol: __timedwait_cp
|
||||
>>> defined at ovr2.c
|
||||
>>> defined at __timedwait.c:34 (.../musl/src/thread/__timedwait.c:34)
|
||||
```
|
||||
|
||||
### 7.2 为什么链接期接管有效
|
||||
|
||||
- 「hidden」不阻止静态链接接管:lld 按归档顺序解析,`meow_ui_time.o` 先于 `libc.a` 参与,
|
||||
libc 内部对 `__timedwait_cp` 的引用就绑到本文(实测 `jal 0x…<__timedwait_cp>`)。
|
||||
- 前提是 `meow_ui_time.o` 被拉入:SDK/App 都引用 `meow_ui_time_ms()`,且 `clock_gettime`
|
||||
被重定向到 `__clock_gettime64`(本文也定义),所以它总会被拉入。**若将来把本 TU 的
|
||||
接管符号拆到没人引用的新 TU,接管会静默失效。**
|
||||
- `__eintr_valid_flag` 给弱定义,libc `signal/sigaction.c` 的强定义优先(同时链 sigaction
|
||||
实测不冲突)。
|
||||
- 取消点语义不重写:`__timedwait_cp` 仍调 libc 的 `__syscall_cp`,`ECANCELED`/`EINTR`
|
||||
处理逐行照抄 musl。
|
||||
- 结构宽度前提:本 TU 与被接管入口的调用者必须用同一套 zig 头(`time_t` 8B、
|
||||
`struct timespec` 16B)。当前四 App 与 launcher 都用 `zig-mipsel-cc` 编译
|
||||
(`$SYSROOT/lib` 只有 `libz.a`,libc 来自 zig 内置 musl),前提成立;若将来换链,
|
||||
必须整链替换、不能混用。
|
||||
|
||||
### 7.3 未修项的配方(同一模式,约 10 行/项)
|
||||
|
||||
1. 在 `meow_ui_time.c` 的 mips 分支里定义 `__<api>_time64`(或该 API 的重定向名);
|
||||
2. 入参展成 `meow_time64_spec` / `meow_time64_itimerspec`,调
|
||||
`meow_time32_rel_from_parts()`(超时类,饱和)或 `meow_time32_abs_from_parts()`
|
||||
(绝对时刻类,越界回 `ENOTSUP`);
|
||||
3. 发 `SYS_<api>_time32`(或 `SYS_<api>` 的 time32 号),输出再反向展开;
|
||||
4. 同一 `.o` 里 libc 定义的所有符号一起接管(见 7.1 表),否则重复定义;
|
||||
5. 宿主单测补边界,交叉反汇编确认结构宽度与 syscall 号。
|
||||
|
||||
不建议顺手全改:这些 API 当前零调用(全仓 `grep` 只有 ncm 的宿主 bench/测试用 `utimensat`),
|
||||
每多接管一个就多一份「转换写错 → 静默写坏文件时间/信号等待」的面;优先级应按「谁先被 App
|
||||
用」来定。根治仍是上游:换容器旧链(gcc 不定义 `__ILP32__`)编 SDK,或修补 zig 内置 musl
|
||||
的 `!__ILP32__` 守卫(本意只排除 x32)。
|
||||
|
||||
## 8. M16 验证
|
||||
|
||||
宿主(aarch64 Termux,gcc):
|
||||
|
||||
```
|
||||
cd patch_v2/sdk
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/m.o # 0 warning
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time64.c /tmp/m.o -o /tmp/t && /tmp/t
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/m.o -o /tmp/t0 && /tmp/t0
|
||||
```
|
||||
|
||||
- `test_meow_time64`(新,23 项):相对/绝对换算边界(INT32_MAX±1、INT64_MAX、负值)、
|
||||
itimerspec 四字段不串位与越界原子性,以及「同一块 16B 内存按内核 o32 只读 8B」的
|
||||
before/after 模拟(0.4s → `{0,0}` 立即超时;2.3s → 2s 整)。
|
||||
- `test_meow_time`(M11 回归):换算边界 + 单调前进,仍 PASS。
|
||||
|
||||
交叉:
|
||||
|
||||
```
|
||||
bash patch_v2/sdk/build_sdk_host.sh --verify # 76 成员、全 MIPS32r2、0 warning
|
||||
```
|
||||
|
||||
反汇编(本单新增/改写的入口):
|
||||
|
||||
| 符号 | syscall 号 | 缓冲宽度证据 |
|
||||
| --- | --- | --- |
|
||||
| `__timedwait_cp` | `0x108e` = 4238 `SYS_futex` | 超时在栈上 8B(`addiu $18,$sp,0x20` + `sw $18,0x10($sp)`),不再直传 16B 结构 |
|
||||
| `__timerfd_settime64` | `0x10e3` = 4323 `timerfd_settime32` | 从调用者 32B 结构读 `0x0/0x4/0x8/0x10/0x14/0x18`,打包成栈上 16B |
|
||||
| `__timerfd_gettime64` | `0x10e2` = 4322 | 内核只写栈上 16B,再按字段展开到调用者 32B |
|
||||
| `__clock_settime64` | `0x10a6` = 4262 `clock_settime32` | 8B `{int32 sec, int32 nsec}` |
|
||||
|
||||
链接期(真实 `meow_ui.a` + 探针,顺序同 App:`probe.o meow_ui.a … -lc`):
|
||||
|
||||
```
|
||||
llvm-objdump -d probe_sdk | sed -n '/<__pthread_cond_timedwait_time64>:/,/^$/p' | grep jal.__timedwait
|
||||
# cd930: jal 0x419e8 <__timedwait_cp> ← 绑到 meow_ui.a 里的实现
|
||||
llvm-nm probe_sdk | grep -c __timedwait_cp # 1(libc 那份未被拉入)
|
||||
```
|
||||
|
||||
真机验证仍待设备(本单未碰真机):`pthread_cond_timedwait` 传 100ms 超时的等待应真的等
|
||||
约 100ms 再超时,而不是立即返回。
|
||||
@@ -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 字节转换)。
|
||||
+8
-1
@@ -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}"
|
||||
@@ -16,7 +21,9 @@ rm -f meow_ui.a *.o
|
||||
# meow_font.c = 预渲染位图字体运行时(自包含:只含 meow_font.h + 标准头;最小 zlib ABI 自声明,
|
||||
# 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。新模块(box/anim/scroll/str + cachegov/lazygrid/
|
||||
# select/net_ipc)随主仓增量并入。
|
||||
OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_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 =="
|
||||
|
||||
+102
-10
@@ -81,10 +81,20 @@ 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,分片) */
|
||||
/* ---- 系统翻页信号(READER_UI_DESIGN §E,用户 2026-09-04 拍板):0x0080-0x0081 ----
|
||||
* 语义:launcher 按键层(meow_launcherd/keys.c)在「当前台 App 为其 app.json 声明
|
||||
* volume_page 的前台应用」时,把物理 VOL_UP/VOL_DOWN 转换成系统翻页信号下发(否则照旧
|
||||
* 调音量——音量基本行为/插孔消抖/音量反转绝不破坏)。App 侧(如 reader_app/app/reader_ipc.c)
|
||||
* 持持久连接收本消息 → 上一页/下一页。方向由消息号本身决定(UP=上一页,DOWN=下一页);
|
||||
* 无负载(len=0)。属系统级能力,未来其它 App 可声明复用。 */
|
||||
MSG_KEY_PAGEUP = 0x0080, /* ↓ 系统级:上一页(VOL_UP 语义,仅前台声明 app 收) */
|
||||
MSG_KEY_PAGEDOWN = 0x0081, /* ↓ 系统级:下一页(VOL_DOWN 语义,仅前台声明 app 收) */
|
||||
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
|
||||
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
|
||||
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
|
||||
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义) */
|
||||
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义):后台保活表命中→就地真退序列;
|
||||
* 否则有前台会话→置 pending 交会话消费;都没有→ACK -2(MEOW_ERR_NOENT) */
|
||||
/* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ----
|
||||
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
|
||||
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
|
||||
@@ -99,6 +109,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:
|
||||
@@ -274,6 +288,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 默认图标回退);
|
||||
@@ -427,7 +464,9 @@ typedef struct m_ams_start {
|
||||
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
|
||||
} m_ams_start_t; /* 160 B */
|
||||
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。 */
|
||||
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。
|
||||
* 后台保活 App:launcher 就地走 meow_bg_terminate 真退序列(与后台管理页「杀」同一通道);
|
||||
* 前台 App:置 pending 交前台会话消费;两者皆非 → ACK code=(uint32_t)MEOW_ERR_NOENT(-2)。 */
|
||||
typedef struct m_ams_kill {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
} m_ams_kill_t; /* 32 B */
|
||||
@@ -457,24 +496,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_* 为实际授权值);
|
||||
@@ -505,11 +552,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:
|
||||
@@ -607,6 +667,38 @@ 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,8 +23,65 @@ 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。
|
||||
|
||||
@@ -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) */
|
||||
/* ============================================================ */
|
||||
|
||||
@@ -49,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 {
|
||||
@@ -145,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,
|
||||
@@ -254,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):行=图标+标题+副标题+右侧操作 + 选中
|
||||
@@ -300,11 +316,16 @@ typedef struct meow_list_item {
|
||||
/* 行数据源回调:idx 有效时填充 out 并返回 1;越界返回 0。每行轻量填充(不做 IO,§5.5)。 */
|
||||
typedef int (*meow_list2_source)(int idx, meow_list_item *out, void *ctx);
|
||||
|
||||
/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。行高默认 56(可用 set_item_h 改)。 */
|
||||
/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。
|
||||
* 行高默认按当前主题字体行高派生(双行 max(56, 2*行高+2) / 单行 max(48, 行高)),
|
||||
* 20px 字库(行高 30)下双行=62;显式 set_item_h 可覆盖(App 自管卡高时用)。 */
|
||||
meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
|
||||
meow_list2_source src, void *ctx, meow_list_sel mode);
|
||||
/* 行高(固定;越界/<=0 忽略)。 */
|
||||
/* 行高:显式 px(>0)优先并固定;越界/<=0 忽略(保持字体派生)。返回当前生效行高。 */
|
||||
int meow_listview_set_item_h(meow_widget *lv, int item);
|
||||
/* 当前生效行高(显式值,或未显式时按主题字体行高派生的值)。无效控件返回 0。
|
||||
* 行几何(draw/hit/max_scroll)全部按此值,App 自绘行装饰用它对齐,勿再硬编码 56。 */
|
||||
int meow_listview_item_h(meow_widget *lv);
|
||||
/* 行数(搜索过滤/懒加载/目录切换时动态更新):截断 sel 到新范围 + clamp scroll。 */
|
||||
void meow_listview_set_count(meow_widget *lv, int count);
|
||||
/* 滚动:set 越界 clamp 到 [0, max_scroll];by 相对滚动(App on_swipe/touch_move 用)。 */
|
||||
@@ -378,11 +399,17 @@ int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count,
|
||||
* 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)互补。 */
|
||||
/* 行档位(默认行高口径):lines=1→max(48, 行高)、2→max(56, 2*行高+2),由当前字体行高派生
|
||||
* (与 set_item_h 互补:tier 恢复派生默认,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);
|
||||
|
||||
|
||||
+1
-3
@@ -62,9 +62,7 @@ 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;
|
||||
return (int64_t)meow_ui_time_ms();
|
||||
}
|
||||
void meow_cachegov_test_set_time_ms(int64_t t) { g_test_now_ms = t; }
|
||||
|
||||
|
||||
@@ -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 */
|
||||
+1056
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 */
|
||||
+24
-13
@@ -268,35 +268,46 @@ int meow_lazygrid_hit(meow_lazygrid *g, int x, int y)
|
||||
}
|
||||
|
||||
/* ---- 绘制(只画可见行;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;
|
||||
uint32_t bg = sel ? th->primaryContainer : th->surfaceContainerLow;
|
||||
meow_gfx_fill_round(cell_x, cell_y, g->cell_w, g->cell_h, th->radius_card, bg);
|
||||
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);
|
||||
int chh = g->cell_h, cw = g->cell_w;
|
||||
/* 图标(可选):居中于格顶 ~50%。 */
|
||||
/* 图标(可选):居中于卡顶 ~50%(卡中心=格中心,因对称内缩)。 */
|
||||
if (it->icon) {
|
||||
int isz = cw * 6 / 10; if (isz > chh * 5 / 10) isz = chh * 5 / 10;
|
||||
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 = cell_x + cw / 2, icy = cell_y + pad + isz / 2;
|
||||
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 = cell_y + chh - pad - thh - (has_sub ? (thh + 2) : 0);
|
||||
if (it->title) meow_ui_text_center(cell_x + cw / 2, ty, it->title, th->font_body, txtcol);
|
||||
if (has_sub) meow_ui_text_center(cell_x + cw / 2, ty + thh + 2, it->subtitle, th->font_body, subcol);
|
||||
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(cell_x, cell_y, cw, chh, th->radius_card, th->primary);
|
||||
/* 选中描边(轮廓);禁用卡内变淡(圆角 α,防圆角外叠出灰方块)。 */
|
||||
if (sel) meow_ui_draw_rect_round(cx, cy, cw, chh, th->radius_card, th->primary);
|
||||
if (!it->enabled)
|
||||
meow_ui_fill_rect_alpha(cell_x, cell_y, cw, chh, th->onSurface, 90);
|
||||
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)
|
||||
|
||||
@@ -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,544 @@
|
||||
/*
|
||||
* 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>
|
||||
#include <limits.h> /* LONG_MAX:BMP 按行 seek 的 32 位偏移上限 */
|
||||
|
||||
/* ---------------- 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
|
||||
|
||||
/* ---------------- ILP32 尺寸算术守卫 ----------------
|
||||
* mipsel / arm-linux-musleabi 上 long 与 size_t 都是 32 位:把「声明尺寸 × bpp」这类
|
||||
* 乘积直接放在 long 里会回绕成小值或 0。实测(ARM32):54 字节 BMP 头声明 w=2^27、
|
||||
* bpp=32 → w*bpp = 2^32 = 0 → rowsize = 0 → malloc(0) → 采样索引 row + sx*4 越界读
|
||||
* → SIGSEGV;同一文件在 LP64 宿主返回 NULL,所以宿主测试天然看不到这类。
|
||||
* 因此:凡「由文件头算出来的字节数/偏移」一律升到 unsigned long long 再验,
|
||||
* 超限即返回 0,由调用方回退 stb(安全失败,与 M26 之前的行为一致)。
|
||||
* 单边上限 1<<20(1,048,576):远超真实 BMP/PNG 声明尺寸,同时保证行字节数
|
||||
* (≤4 MiB)、行偏移与目标 RGBA 都不会在 32 位溢出。 */
|
||||
#define MZP_MAX_DIM (1u << 20)
|
||||
static int mzp_bytes_ok(unsigned long long n)
|
||||
{
|
||||
return n > 0 && n <= (unsigned long long)(size_t)-1;
|
||||
}
|
||||
|
||||
/* ---------------- 字节源 ---------------- */
|
||||
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 long sy = (long long)c->next_ty * (long long)c->h / c->th; /* 64 位:ILP32 上 *h 会溢出 */
|
||||
if (sy == (long long)c->y) {
|
||||
unsigned char *o = c->out +
|
||||
(size_t)((unsigned long long)c->next_ty * (unsigned long long)c->tw * 4u);
|
||||
int tx;
|
||||
for (tx = 0; tx < c->tw; tx++) {
|
||||
long long sxl = (long long)tx * (long long)c->w / c->tw;
|
||||
int sx = (int)(sxl < 0 ? 0
|
||||
: (sxl >= (long long)c->w ? (long long)c->w - 1 : sxl));
|
||||
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);
|
||||
{
|
||||
/* ILP32 上 tw*th*4 会回绕(32768×32768×4 = 2^32 → 0 → malloc(0) 后越界写) */
|
||||
unsigned long long ob = (unsigned long long)c.tw * (unsigned long long)c.th * 4u;
|
||||
if (!mzp_bytes_ok(ob)) goto done;
|
||||
c.out = (unsigned char *)malloc((size_t)ob);
|
||||
if (!c.out) goto done;
|
||||
memset(c.out, 0, (size_t)ob);
|
||||
}
|
||||
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;
|
||||
bpp = le16(dib + 14);
|
||||
compression = le32(dib + 16);
|
||||
colors = (dibsize >= 36) ? le32(dib + 32) : 0;
|
||||
if (compression != 0) return 0; /* RLE/BITFIELDS → stb */
|
||||
if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0;
|
||||
/* 声明尺寸过 32 位算术闸(M26 r1 P1):w×bpp 与 tx×w 在 ILP32 上会回绕成小值/负数,
|
||||
* 直接导致 malloc(0) + 越界读。h=INT_MIN 时 -h 同样溢出,故升到 long long 再取绝对值。 */
|
||||
{
|
||||
int wi = (int)le32(dib + 4);
|
||||
int hi = (int)le32(dib + 8);
|
||||
long long hll;
|
||||
if (wi <= 0 || hi == 0) return 0;
|
||||
topdown = hi < 0;
|
||||
hll = topdown ? -(long long)hi : (long long)hi;
|
||||
if ((unsigned long long)wi > MZP_MAX_DIM || hll <= 0 ||
|
||||
(unsigned long long)hll > MZP_MAX_DIM) return 0; /* 离奇声明尺寸 → 回退 stb */
|
||||
w = (long)wi;
|
||||
h = (long)hll;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
{
|
||||
/* 行字节数 64 位算(ILP32 上 w*bpp 在 w≥2^27/bpp 时回绕成 0) */
|
||||
unsigned long long rb = ((unsigned long long)w * bpp + 31u) / 32u * 4u;
|
||||
if (!mzp_bytes_ok(rb)) return 0;
|
||||
rowsize = (long)rb; /* w ≤ MZP_MAX_DIM → ≤ 4 MiB */
|
||||
}
|
||||
meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_);
|
||||
row = (unsigned char *)malloc((size_t)rowsize);
|
||||
{
|
||||
unsigned long long ob = (unsigned long long)tw_ * (unsigned long long)th_ * 4u;
|
||||
if (mzp_bytes_ok(ob)) out = (unsigned char *)malloc((size_t)ob);
|
||||
}
|
||||
if (!row || !out) goto done;
|
||||
|
||||
for (int ty = 0; ty < th_; ty++) {
|
||||
long long sy = (long long)ty * h / th_;
|
||||
long long file_row = topdown ? sy : ((long long)h - 1 - sy);
|
||||
long long off = (long long)dataoff + file_row * (long long)rowsize;
|
||||
unsigned char *o = out + (size_t)((unsigned long long)ty * (unsigned long long)tw_ * 4u);
|
||||
if (src.mem) {
|
||||
if (off < 0 ||
|
||||
(unsigned long long)off + (unsigned long long)rowsize > (unsigned long long)src.memlen)
|
||||
goto done;
|
||||
memcpy(row, src.mem + (size_t)off, (size_t)rowsize);
|
||||
} else {
|
||||
if (off < 0 || off > (long long)LONG_MAX) goto done; /* fseek 只有 32 位偏移 */
|
||||
if (fseek(src.f, (long)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++) {
|
||||
long long sxl = (long long)tx * w / tw_;
|
||||
int sx = (int)(sxl < 0 ? 0 : (sxl >= (long long)w ? (long long)w - 1 : sxl));
|
||||
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;
|
||||
}
|
||||
+4
-1
@@ -103,7 +103,10 @@ int meow_select_open(meow_select *s)
|
||||
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);
|
||||
/* MD3 sheet 顶角 = corner-extra-large-top(28)(material-web _md-comp-sheet-bottom.scss),
|
||||
* 与 Dialog 一致;同 SDK meow_sheet(meow_ui_dialog.c:361)/dialog 卡(meow_ui_dialog.c:103) 取 28,
|
||||
* 底部面板统一 extra-large,避免同 SDK 两个 bottom sheet 顶部圆角不一致(旧 th->radius_card=16)。 */
|
||||
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);
|
||||
meow_widget_draw(lv);
|
||||
meow_ui_flush();
|
||||
|
||||
+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);
|
||||
|
||||
@@ -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) {
|
||||
@@ -134,12 +134,13 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g)
|
||||
y += meow_ui_text_height(th->font_body) + 8;
|
||||
}
|
||||
}
|
||||
/* 取消(左,text 型)/ 确定(右,filled) */
|
||||
/* 取消(左,outlined)/ 确定(右,filled)——MD3 按钮统一为胶囊(full,全高一半),
|
||||
* 与 SDK widget_btn_radius(meow_ui_widget.c:146) 同款;旧硬编码 radius 10 非 token。 */
|
||||
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_draw_rect_round(bx, by, g->btn_w, g->btn_h, MEOW_SHAPE_FULL(g->btn_h), th->primary);
|
||||
meow_ui_text_center(bx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2,
|
||||
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_gfx_fill_round(cx, by, g->btn_w, g->btn_h, MEOW_SHAPE_FULL(g->btn_h), th->primary);
|
||||
meow_ui_text_center(cx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2,
|
||||
meow_str("common.ok"), th->font_body, th->onPrimary);
|
||||
}
|
||||
@@ -358,7 +359,7 @@ int meow_sheet_open(meow_sheet *s)
|
||||
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;
|
||||
|
||||
+184
-26
@@ -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));
|
||||
@@ -830,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)
|
||||
{
|
||||
@@ -852,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)
|
||||
{
|
||||
@@ -884,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; }
|
||||
@@ -921,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) */
|
||||
}
|
||||
@@ -963,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];
|
||||
@@ -981,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;
|
||||
}
|
||||
|
||||
@@ -995,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;
|
||||
@@ -1031,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 刚叠画的顶条冲成未初始化数据——真机
|
||||
@@ -1068,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++;
|
||||
|
||||
@@ -75,6 +75,16 @@ static int g_ime_done = 0;
|
||||
static int g_ime_result = 0;
|
||||
static int g_shift = 0;
|
||||
|
||||
/* WiFi 设置页 bug3(2026-09-15):IME 打开瞬间 TAP 冷却窗。触发打开的那次点按,其「抬起
|
||||
* TAP」(含 goodix 抬起长尾)可能由 SDK 输入线程在打开 drain 之后才读出并入队(子页自有
|
||||
* tfd 与 SDK 输入线程独立读同一 evdev,节奏可滞后 ~20ms+)——阻塞循环把它当「点背景=取消」
|
||||
* 瞬间关键盘(y<KB_TOP)或幻影按键(y≥KB_TOP)。窗口期内的 TAP 一律消费丢弃(镜像
|
||||
* meow_ui_run 的 STARTUP_COOLDOWN_MS 500 先例,取 200 兼顾手感;正常打字第一键必在窗后)。
|
||||
* 防御为双保险:drain 已复位触摸状态机(在途按压不再产 TAP),冷却窗再兜住 drain 前已入队
|
||||
* 未及清/时序竞争残漏的 TAP。 */
|
||||
#define IME_TAP_COOLDOWN_MS 200
|
||||
static uint64_t g_ime_open_ms = 0; /* meow_ui_time_ms() 单调毫秒;0=未记录 */
|
||||
|
||||
static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */
|
||||
static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */
|
||||
static char g_ime_buf[256];
|
||||
@@ -570,6 +580,11 @@ int meow_ime_long_press(int x, int y)
|
||||
int meow_ime_handle_event(const meow_input_ev *e)
|
||||
{
|
||||
if (!g_ime_active || g_ime_done) return 0;
|
||||
/* 打开冷却窗(bug3):窗口内 TAP 一律消费丢弃——防触发打开那次点按的迟到「抬起 TAP」
|
||||
* 被当「点背景=取消」或幻影按键。消费(return 1)而非透传:事件已属 IME 会话期。 */
|
||||
if (e->type == MEOW_EV_TAP && g_ime_open_ms &&
|
||||
meow_ui_time_ms() - g_ime_open_ms < IME_TAP_COOLDOWN_MS)
|
||||
return 1;
|
||||
switch (e->type) {
|
||||
case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b);
|
||||
case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */
|
||||
@@ -605,6 +620,11 @@ void meow_ui_text_input_async(meow_ime_ctx *ctx)
|
||||
ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */
|
||||
meow_input_start();
|
||||
meow_input_drain();
|
||||
/* bug3 加固(对齐 s_dd_modal/subpage 吸收 goodix 抬起长尾的「排空→40ms→再排空」惯例):
|
||||
* 第一遍 drain 后等 40ms 让输入线程读出迟到事件,第二遍 drain 连状态机一并复位。 */
|
||||
usleep(40000);
|
||||
meow_input_drain();
|
||||
g_ime_open_ms = meow_ui_time_ms(); /* TAP 冷却窗起点(见 IME_TAP_COOLDOWN_MS) */
|
||||
}
|
||||
|
||||
int meow_ui_text_input(meow_ime_ctx *ctx)
|
||||
|
||||
+156
-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);
|
||||
}
|
||||
|
||||
+87
-42
@@ -27,6 +27,7 @@
|
||||
#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>
|
||||
@@ -57,6 +58,8 @@
|
||||
|
||||
#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
|
||||
@@ -65,12 +68,18 @@
|
||||
|
||||
/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */
|
||||
void meow_ui_gesture_end(void);
|
||||
/* 触摸状态机复位(定义在触摸状态机变量之后,meow_input_drain 内调用)。 */
|
||||
void meow_input_touch_reset(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、
|
||||
@@ -80,6 +89,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;
|
||||
@@ -99,13 +122,6 @@ static uint64_t last_tick_ms = 0;
|
||||
static uint64_t startup_cooldown_until = 0;
|
||||
#define STARTUP_COOLDOWN_MS 500
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */
|
||||
static const char *meow_settings_path(void)
|
||||
{
|
||||
@@ -140,28 +156,17 @@ static int meow_input_jack_debounce(void)
|
||||
return g_jack_changed_at && (now - g_jack_changed_at) < 800;
|
||||
}
|
||||
|
||||
/* ---- §10.5 叠加让位:通知中心叠加会话期输入排空(播放中 App 不被 SIGSTOP 的配套)----
|
||||
/* ---- §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 前台也生效)----
|
||||
@@ -202,6 +207,10 @@ void meow_input_drain(void)
|
||||
g_qh = 0; g_qt = 0;
|
||||
pthread_mutex_unlock(&g_qm);
|
||||
meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */
|
||||
/* WiFi 设置页 bug3(2026-09-15):drain 同时复位触摸状态机——语义自洽:drain=丢弃在途
|
||||
* 手势。否则 drain 前已开始、drain 后才被输入线程读出的「抬起」仍会按残留原点产生
|
||||
* 一个迟到 TAP 入队(IME 打开瞬间被当「点背景=取消」或幻影按键,键盘开即关)。 */
|
||||
meow_input_touch_reset();
|
||||
}
|
||||
|
||||
static void push_ev(int type, int a, int b)
|
||||
@@ -250,6 +259,19 @@ static int t_mv_x = -1, t_mv_y = -1;
|
||||
/* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */
|
||||
static int t_long_fired = 0;
|
||||
|
||||
/* 触摸状态机整体复位:丢弃在途手势(down/原点/坐标/长按触发标记全部归零)。
|
||||
* 调用点:① meow_input_drain(drain=丢弃在途手势,IME/模态打开前);② §10.5 叠加抑制
|
||||
* 进入沿(防抑制期丢失抬手致 t_down 卡死)。效果:复位前已开始、复位后才抬起的按压
|
||||
* 不再产生 TAP/SWIPE(WiFi 设置页 bug3:IME 打开 drain 之后才入队的迟到「抬起 TAP」
|
||||
* 曾把键盘当「点背景=取消」瞬间关闭)。 */
|
||||
void meow_input_touch_reset(void)
|
||||
{
|
||||
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
|
||||
t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = 0;
|
||||
t_mv_x = -1; t_mv_y = -1;
|
||||
t_long_fired = 0;
|
||||
}
|
||||
|
||||
/* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */
|
||||
static int g_long_press_ms = MEOW_LONG_PRESS_MS;
|
||||
/* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */
|
||||
@@ -265,7 +287,7 @@ static void touch_press(int x, int y)
|
||||
t_origin_valid = 0;
|
||||
t_cx = -1; t_cy = -1;
|
||||
t_dx = 0; t_dy = 0;
|
||||
t_down_ms = now_ms(); /* 记按下时刻:供实时按住时长/按住视觉 */
|
||||
t_down_ms = meow_ui_time_ms(); /* 记按下时刻:供实时按住时长/按住视觉 */
|
||||
t_mv_x = -1; t_mv_y = -1; /* M2.71:拖动过程起点未定 */
|
||||
t_long_fired = 0; /* §6.3:每次新按压复位长按触发标记 */
|
||||
}
|
||||
@@ -337,7 +359,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);
|
||||
@@ -354,7 +385,7 @@ int meow_input_touch_y(void) { return t_cy; }
|
||||
unsigned int meow_input_touch_hold_ms(void)
|
||||
{
|
||||
if (!t_down) return 0;
|
||||
uint64_t e = now_ms() - t_down_ms;
|
||||
uint64_t e = meow_ui_time_ms() - t_down_ms;
|
||||
if (e > 0xFFFFFFFFu) e = 0xFFFFFFFFu;
|
||||
return (unsigned int)e;
|
||||
}
|
||||
@@ -365,7 +396,7 @@ void meow_input_touch_set(int down, int x, int y, unsigned hold_ms)
|
||||
if (down) {
|
||||
t_down = 1; t_press_valid = 1; t_origin_valid = 1; t_long_fired = 0;
|
||||
t_cx = x; t_cy = y; t_dx = x; t_dy = y;
|
||||
uint64_t now = now_ms();
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
t_down_ms = (now > (uint64_t)hold_ms) ? (now - (uint64_t)hold_ms) : 0;
|
||||
} else {
|
||||
t_down = 0; t_press_valid = 0; t_origin_valid = 0; t_long_fired = 0;
|
||||
@@ -506,7 +537,7 @@ static uint64_t g_sys_press_ms = 0;
|
||||
|
||||
static void sys_key_press(int code)
|
||||
{
|
||||
uint64_t now = now_ms();
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (code == g_sys_press_code && now - g_sys_press_ms < SYS_KEY_DEDUP_MS)
|
||||
return; /* 事件流 + EVIOCGKEY 双源去重:同码窗口内只消化一次 */
|
||||
g_sys_press_code = code;
|
||||
@@ -573,9 +604,7 @@ static void *input_thread(void *arg)
|
||||
if (overlay_input_suppressed()) {
|
||||
if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */
|
||||
overlay_was = 1;
|
||||
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
|
||||
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
|
||||
t_long_fired = 0;
|
||||
meow_input_touch_reset();
|
||||
}
|
||||
if (r > 0)
|
||||
for (int i = 0; i < nfd; i++) {
|
||||
@@ -666,7 +695,7 @@ void meow_ui_request_exit(void) { g_exit = 1; }
|
||||
static uint64_t g_activity_last_ms = 0;
|
||||
void meow_ui_activity_ping(void)
|
||||
{
|
||||
uint64_t now = now_ms();
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (now - g_activity_last_ms < 500) return; /* 节流:任意交互连续触摸也只 ~2 次/s */
|
||||
g_activity_last_ms = now;
|
||||
if (!meow_gfx_is_device()) return; /* 离屏/宿主:无 launcher 消费 */
|
||||
@@ -679,8 +708,8 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
if (ev) { g_events = *ev; g_events_set = 1; }
|
||||
g_exit = 0;
|
||||
meow_input_start();
|
||||
last_tick_ms = now_ms();
|
||||
startup_cooldown_until = now_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */
|
||||
last_tick_ms = meow_ui_time_ms();
|
||||
startup_cooldown_until = meow_ui_time_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */
|
||||
|
||||
while (!g_exit) {
|
||||
/* agent-119:实时触摸按压/按住/释放 → 即时重绘命中按钮区(不等 app 重绘)。
|
||||
@@ -688,10 +717,10 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
* §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动;启动窗口不触发)。 */
|
||||
if (!meow_ime_active()) {
|
||||
meow_widget_touch_refresh();
|
||||
if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll();
|
||||
if (meow_ui_time_ms() >= startup_cooldown_until) meow_input_long_press_poll();
|
||||
} else if (meow_ime_mode_pinyin()) {
|
||||
/* 拼音 IME 打开:喂长按轮询(长按退格清空字段)。英文模式不喂(保持既有行为)。 */
|
||||
if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll();
|
||||
if (meow_ui_time_ms() >= startup_cooldown_until) meow_input_long_press_poll();
|
||||
}
|
||||
|
||||
meow_input_ev e;
|
||||
@@ -700,7 +729,7 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
if (g_exit) break; /* 已请求退出:不再分发剩余事件,杜绝「退出后又触发」的偶发再进/退 */
|
||||
got = 1;
|
||||
meow_ui_activity_ping(); /* M2.71-bug5:任意交互事件都喂 launcher 重置息屏计时(节流) */
|
||||
int in_startup = (now_ms() < startup_cooldown_until); /* M2.22(C) */
|
||||
int in_startup = (meow_ui_time_ms() < startup_cooldown_until); /* M2.22(C) */
|
||||
if (e.type == MEOW_EV_TAP) {
|
||||
if (in_startup) continue; /* 启动窗口也吞 TAP:旧手势残留点按(如上一次左滑抬起)
|
||||
* 在进入瞬间命中 UI 按钮(如全屏按钮)会误触发——丢弃。 */
|
||||
@@ -746,6 +775,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; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */
|
||||
/* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发
|
||||
@@ -799,7 +844,7 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t now = now_ms();
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (g_events_set && g_events.on_tick) {
|
||||
if (now - last_tick_ms >= 16) {
|
||||
last_tick_ms = now;
|
||||
@@ -825,7 +870,7 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
|
||||
if (meow_ime_active()) { /* 软键盘覆盖层随帧绘制(30fps 节流,省 CPU) */
|
||||
static uint64_t last_ime_ms = 0;
|
||||
uint64_t n = now_ms();
|
||||
uint64_t n = meow_ui_time_ms();
|
||||
if (n - last_ime_ms >= 33) {
|
||||
last_ime_ms = n;
|
||||
meow_ime_draw();
|
||||
@@ -845,7 +890,7 @@ void meow_ui_run(const meow_ui_events *ev)
|
||||
void meow_ui_feed_tick(void)
|
||||
{
|
||||
if (!(g_events_set && g_events.on_tick)) return;
|
||||
uint64_t now = now_ms();
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (now - last_tick_ms < 16) return;
|
||||
last_tick_ms = now;
|
||||
g_events.on_tick();
|
||||
|
||||
+67
-7
@@ -131,12 +131,6 @@ void meow_gfx_mark_full(void);
|
||||
* 菜单内容盖到 App 内容上)——叠加前用本函数撤销整帧脏,再按需标局部脏(agent-146)。 */
|
||||
void meow_gfx_dirty_reset(void);
|
||||
|
||||
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
|
||||
unsigned char *meow_ui_back_buf(void);
|
||||
/* back_buf 的每行字节数(pitch)与位深(16/32)。M2.71-bug3:模态/IME 快照恢复用。 */
|
||||
int meow_ui_back_buf_pitch(void);
|
||||
int meow_ui_back_buf_bpp(void);
|
||||
|
||||
/* ---- 模态背景快照/恢复(meow_ui_dialog.c 提供;dialog/sheet/select 共用)----
|
||||
* open 前调用 capture 把整帧背景拍下(modal_capture),close 后调用 restore 回填 + 整帧上屏
|
||||
* (modal_restore)→ scrim 每次只叠一层、关闭即还原无残影(M2.71-bug3)。共享同一份快照缓冲,
|
||||
@@ -144,6 +138,12 @@ int meow_ui_back_buf_bpp(void);
|
||||
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 快照恢复用。 */
|
||||
int meow_ui_back_buf_pitch(void);
|
||||
int meow_ui_back_buf_bpp(void);
|
||||
|
||||
/* 实心圆角矩形(控件按钮底色用;公共 §3 只有空心 draw_rect_round)。[D3] */
|
||||
void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color);
|
||||
/* 圆角矩形半透明填充(M2.71-bug4):禁用变暗等状态层用,圆角外不叠色(防灰方块)。 */
|
||||
@@ -163,9 +163,29 @@ 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);
|
||||
|
||||
/* ---- 矢量字(FreeType)渲染薄封装(meow_ui_gfx.c 提供 / meow_ui_ft.c 使用)----
|
||||
* meow_ui_ft.o 是独立成员:App 显式调 meow_ft_* 才被拉取,随之才拉入 freetype .o。
|
||||
* 为让「不调 meow_ft_* 的 App 零增量」(gfx.o 不引用 meow_ui_ft.o / FT 符号),gfx.o 只
|
||||
* 提供下面两个绘制原语薄封装(blend 一个灰度像素进 back_buf、标一块脏区)。 */
|
||||
void meow_gfx_ft_blend_px(int x, int y, uint32_t color, int alpha);
|
||||
void meow_gfx_ft_dirty(int x, int y, int w, int h);
|
||||
|
||||
/* ---- 矢量字(FreeType)字形访问(meow_ui_ft.c 提供;宿主单测直接调)----
|
||||
* 返回 0=成功;-1=无已载字体(调用方应降级位图字库)。位图归 LRU 缓存所有,本次用完前
|
||||
* 勿再调 meow_ft_glyph(同字号新光栅化可能逐出旧槽)。 */
|
||||
int meow_ft_glyph(int px, uint32_t cp, int *adv_x,
|
||||
const uint8_t **bmp, int *bw, int *bh, int *left, int *top);
|
||||
int meow_ft_advance(int px, uint32_t cp, int *adv_x); /* 仅测宽不栅格化、不占 LRU 槽 */
|
||||
|
||||
/* ---- 输入层(meow_ui_input.c)---- */
|
||||
/* 语义事件:由输入线程解析后压入队列,run/IME 循环弹出。 */
|
||||
#define MEOW_EV_KEY 1 /* a=keycode, b=1 按下 / 0 抬起 */
|
||||
@@ -187,8 +207,10 @@ void meow_input_start(void);
|
||||
int meow_input_pop(meow_input_ev *ev);
|
||||
/* 注入一条语义事件(宿主验证手势分发/进度条用;设备上无调用方)。 */
|
||||
void meow_input_inject(int type, int a, int b);
|
||||
/* 清空队列(IME 打开前丢弃残留触摸,避免误敲)。 */
|
||||
/* 清空队列(IME 打开前丢弃残留触摸,避免误敲);同时复位触摸状态机(drain=丢弃在途手势)。 */
|
||||
void meow_input_drain(void);
|
||||
/* 触摸状态机整体复位(丢弃在途手势;drain/叠加抑制进入沿调用;宿主单测可直调)。 */
|
||||
void meow_input_touch_reset(void);
|
||||
|
||||
/* ---- M2.71-bug5:App 活动 → 通知 launcher 重置息屏计时 ----
|
||||
* launcher 前台 app 会话的 app_last_input 只在物理键边沿重置(它看不到应用内触摸)。SDK 在
|
||||
@@ -278,4 +300,42 @@ 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);
|
||||
|
||||
/* ---- mipsel o32 的 time32 结构(内核布局,与 libc 头的 time_t 宽度无关)----
|
||||
* 背景:zig clang 给 32 位目标预定义 __ILP32__,打穿 musl 的 time64 回退守卫,
|
||||
* 一批封装把 16 字节 time64 结构直接交给 time32 系统调用(CLOCK_ABI.md §6)。
|
||||
* 接管这些入口时统一走下面这几个纯转换函数——它们不依赖平台,宿主单测直接覆盖。 */
|
||||
struct meow_time32 {
|
||||
int32_t tv_sec;
|
||||
int32_t tv_nsec;
|
||||
};
|
||||
struct meow_itimerspec32 {
|
||||
struct meow_time32 it_interval;
|
||||
struct meow_time32 it_value;
|
||||
};
|
||||
/* 与 libc 结构解耦的 64 位镜像(避免宿主/交叉两端 time_t 宽度差异进测试)。 */
|
||||
struct meow_time64_spec {
|
||||
int64_t tv_sec;
|
||||
int64_t tv_nsec;
|
||||
};
|
||||
struct meow_time64_itimerspec {
|
||||
struct meow_time64_spec it_interval;
|
||||
struct meow_time64_spec it_value;
|
||||
};
|
||||
|
||||
/* 相对超时(futex / poll / select 的「等多久」):sec 超出 int32 时饱和
|
||||
* (正→INT32_MAX、负→INT32_MIN,与 musl CLAMP 一致),nsec 原样。 */
|
||||
void meow_time32_rel_from_parts(int64_t sec, int64_t nsec, struct meow_time32 *out);
|
||||
/* 绝对时刻(clock_settime / timerfd / utimensat):sec 落不进 int32 返回 -1
|
||||
* (调用方回 ENOTSUP,与 musl time64 路径的判定一致),nsec 原样。 */
|
||||
int meow_time32_abs_from_parts(int64_t sec, int64_t nsec, struct meow_time32 *out);
|
||||
/* itimerspec(timerfd / POSIX timer)转换;任一侧越界返回 -1 且 *out 不变。 */
|
||||
int meow_time32_itimerspec_from_64(const struct meow_time64_itimerspec *in,
|
||||
struct meow_itimerspec32 *out);
|
||||
void meow_time64_itimerspec_from_32(const struct meow_itimerspec32 *in,
|
||||
struct meow_time64_itimerspec *out);
|
||||
|
||||
#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)
|
||||
|
||||
+20
-5
@@ -52,11 +52,18 @@ struct meow_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,
|
||||
@@ -70,6 +77,7 @@ meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -81,11 +89,20 @@ void meow_scroll_free(meow_scroll *s)
|
||||
if (s->anim) meow_anim_cancel(s->anim);
|
||||
s->anim = NULL;
|
||||
for (int i = 0; i < MEOW_SCROLL_POOL; i++)
|
||||
if (&g_pool[i] == s) { g_used[i] = 0; return; }
|
||||
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 列表 ---- */
|
||||
typedef struct { meow_widget *w; int lv; } wrap_ctx;
|
||||
static void wrap_set(void *c, int off)
|
||||
{ wrap_ctx *wc = c; if (wc->lv) meow_listview_set_scroll(wc->w, off); else meow_list_set_scroll(wc->w, off); }
|
||||
static int wrap_max(void *c)
|
||||
@@ -100,9 +117,6 @@ static void wrap_redraw(void *c)
|
||||
if (r[2] > 0 && r[3] > 0) meow_ui_flush_region(r[0], r[1], r[2], r[3]); /* 只刷视口区 */
|
||||
}
|
||||
|
||||
static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL];
|
||||
static int g_wrap_used[MEOW_SCROLL_POOL];
|
||||
|
||||
meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview)
|
||||
{
|
||||
if (!list) return NULL;
|
||||
@@ -113,6 +127,7 @@ meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -46,13 +46,6 @@ static int g_statusbar_force = 0; /* 1s 刷新后置位,强制重画顶部
|
||||
* present() 据此决定是否画出顶条/提示线;App 进程保持 0(不渲染)+ 仍 inset 28(launcher 叠加)。 */
|
||||
static int g_sys_render = 0; /* 1=本进程为系统 UI(渲染);0=App(不渲染,仅 inset) */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 读一份 sysfs/文件字符串(剥尾随换行);失败回退 "" */
|
||||
static void read_str(const char *path, char *buf, size_t n)
|
||||
{
|
||||
@@ -163,7 +156,7 @@ void meow_ui_sys_refresh(void)
|
||||
|
||||
/* 蓝牙:hci0 存在性检查便宜;上电状态贵(hciconfig spawn),30s 缓存。 */
|
||||
static uint64_t last_bt_ms = 0;
|
||||
uint64_t n = now_ms();
|
||||
uint64_t n = meow_ui_time_ms();
|
||||
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
|
||||
if (!g_sys.bt_seen) {
|
||||
g_sys.bt_on = 0;
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
/* 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
|
||||
* 转换后重发,恢复亚秒睡眠。
|
||||
*
|
||||
* M16(同源第二弹;逐项证据见 CLOCK_ABI.md §6):同一句 `!__ILP32__` 还打穿了别的入口,
|
||||
* 本文件按风险分档接管:
|
||||
* - futex 相对超时(`__timedwait_cp`/`__timedwait`):**已接管**。它是进程同步核心,
|
||||
* 但 musl 原文逻辑可完整复刻(含 __syscall_cp 取消点语义),且是 SDK/app 唯一在用的
|
||||
* 受害者(meow_ui_audio 的 pthread_cond_timedwait 在亚秒等待下会立即超时)。
|
||||
* - timerfd_settime/gettime、clock_settime:**已接管**(纯参数转换,无内部状态)。
|
||||
* - pselect/ppoll/select、getrusage、wait4、clock_adjtime:**未受影响**(musl 回退分支
|
||||
* 自己就转成 `long[2]`/有宽度判断),本单只补证据,不改代码。
|
||||
* - sigtimedwait、semtimedop、timer_settime/gettime、utimensat、recvmmsg、mq_timedsend、
|
||||
* pthread_mutex_timedlock 的 PI 快路径:仍同源受损但 SDK/App 均未使用,风险收益比不划算,
|
||||
* 修复配方已写进 CLOCK_ABI.md §7 待后续单。
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
#if defined(__mips__) && !defined(__mips64) && (__SIZEOF_LONG__ == 4)
|
||||
#define MEOW_TIME32_KERNEL_ABI 1
|
||||
#include <pthread.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/timerfd.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#define MEOW_TIME32_KERNEL_ABI 0
|
||||
#endif
|
||||
|
||||
/* ---- 平台无关的 time64 → 内核 o32 time32 换算(宿主单测直接覆盖)----
|
||||
* 这几个函数不碰系统调用,只为“结构宽度”这件事提供唯一实现;mips 分支的接管入口
|
||||
* 全部经它们转换,宿主 test_meow_time64.c 用同一份代码验边界。 */
|
||||
void meow_time32_rel_from_parts(int64_t sec, int64_t nsec, struct meow_time32 *out)
|
||||
{
|
||||
if (sec > INT32_MAX) sec = INT32_MAX;
|
||||
else if (sec < INT32_MIN) sec = INT32_MIN;
|
||||
out->tv_sec = (int32_t)sec;
|
||||
out->tv_nsec = (int32_t)nsec;
|
||||
}
|
||||
|
||||
int meow_time32_abs_from_parts(int64_t sec, int64_t nsec, struct meow_time32 *out)
|
||||
{
|
||||
if (sec > INT32_MAX || sec < INT32_MIN) return -1;
|
||||
out->tv_sec = (int32_t)sec;
|
||||
out->tv_nsec = (int32_t)nsec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_time32_itimerspec_from_64(const struct meow_time64_itimerspec *in,
|
||||
struct meow_itimerspec32 *out)
|
||||
{
|
||||
struct meow_itimerspec32 t;
|
||||
if (meow_time32_abs_from_parts(in->it_interval.tv_sec, in->it_interval.tv_nsec,
|
||||
&t.it_interval)) return -1;
|
||||
if (meow_time32_abs_from_parts(in->it_value.tv_sec, in->it_value.tv_nsec,
|
||||
&t.it_value)) return -1;
|
||||
*out = t;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void meow_time64_itimerspec_from_32(const struct meow_itimerspec32 *in,
|
||||
struct meow_time64_itimerspec *out)
|
||||
{
|
||||
out->it_interval.tv_sec = in->it_interval.tv_sec;
|
||||
out->it_interval.tv_nsec = in->it_interval.tv_nsec;
|
||||
out->it_value.tv_sec = in->it_value.tv_sec;
|
||||
out->it_value.tv_nsec = in->it_value.tv_nsec;
|
||||
}
|
||||
|
||||
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 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
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- futex 相对超时(__timedwait_cp / __timedwait 强定义)----
|
||||
* 同源缺陷:musl thread/__timedwait.c 的 __futex4_cp 也在 `#if defined(SYS_futex_time64)
|
||||
* && !defined(__ILP32__)` 里做 time64,被 __ILP32__ 打穿后走 `#else`:
|
||||
* __syscall_cp(SYS_futex(4238), addr, op, val, to) ← to 是 16 字节 time64 结构
|
||||
* 内核按 o32 time32 只读前 8 字节 {int32 sec, int32 nsec}:
|
||||
* 偏移 0..3 = tv_sec 低字(够用),偏移 4..7 = tv_sec 高字(正常计时恒 0)。
|
||||
* 于是「还剩 0.4s」被当成 {0,0} → 立刻超时;「剩 2.3s」被当成 2s 整。
|
||||
* 受害者:pthread_cond_timedwait / sem_timedwait / pthread_join 超时 / aio_suspend
|
||||
* (SDK 的 meow_ui_audio.c 与 meow_audiod、ncm 播放线程都在用)。
|
||||
*
|
||||
* 风险控制:不重写任何同步算法,只把 musl 的 __timedwait_cp 原文搬过来,唯一改动是
|
||||
* 发系统调用前把相对超时转成 time32;取消点仍走 libc 的 __syscall_cp(ECANCELED/
|
||||
* EINTR 语义不变)。链接期接管必须同时定义 __timedwait —— 否则 libc 的 __timedwait.o
|
||||
* 会因 `__timedwait` 未解析被拉入,与本文的 __timedwait_cp 撞成 duplicate symbol
|
||||
* (ld.lld 实测会报错)。__eintr_valid_flag 给弱定义,真正的强定义在 libc sigaction.o。
|
||||
*
|
||||
* 未覆盖:pthread_mutex_timedlock 的 PTHREAD_PRIO_INHERIT 快路径(同文件里的静态
|
||||
* __futex4,直接调 FUTEX_LOCK_PI);SDK/App 未使用 PI 互斥量,见 CLOCK_ABI.md §7。 */
|
||||
#define MEOW_FUTEX_WAIT 0
|
||||
#define MEOW_FUTEX_PRIVATE 128
|
||||
|
||||
int __pthread_setcancelstate(int new, int *old);
|
||||
long __syscall_cp(long nr, long a, long b, long c, long d, long e, long f);
|
||||
__attribute__((weak)) volatile int __eintr_valid_flag = 0;
|
||||
|
||||
static int meow_futex4_cp(volatile void *addr, int op, int val, const struct timespec *to)
|
||||
{
|
||||
struct meow_time32 t32;
|
||||
void *top = 0;
|
||||
long r;
|
||||
|
||||
if (to) {
|
||||
meow_time32_rel_from_parts((int64_t)to->tv_sec, (int64_t)to->tv_nsec, &t32);
|
||||
top = &t32;
|
||||
}
|
||||
r = __syscall_cp(SYS_futex, (long)addr, op, val, (long)top, 0, 0);
|
||||
if (r != -ENOSYS) return (int)r;
|
||||
return (int)__syscall_cp(SYS_futex, (long)addr,
|
||||
op & ~MEOW_FUTEX_PRIVATE, val, (long)top, 0, 0);
|
||||
}
|
||||
|
||||
int __timedwait_cp(volatile int *addr, int val, clockid_t clk,
|
||||
const struct timespec *at, int priv);
|
||||
int __timedwait(volatile int *addr, int val, clockid_t clk,
|
||||
const struct timespec *at, int priv);
|
||||
|
||||
int __timedwait_cp(volatile int *addr, int val, clockid_t clk,
|
||||
const struct timespec *at, int priv)
|
||||
{
|
||||
int r;
|
||||
struct timespec to, *top = 0;
|
||||
|
||||
if (priv) priv = MEOW_FUTEX_PRIVATE;
|
||||
|
||||
if (at) {
|
||||
/* musl 原文是 `>= 1000000000UL`(负值按无符号也判 EINVAL);显式判负以保 -Wextra 0 warning */
|
||||
if (at->tv_nsec < 0 || at->tv_nsec >= 1000000000L) return EINVAL;
|
||||
if (__clock_gettime(clk, &to)) return EINVAL;
|
||||
to.tv_sec = at->tv_sec - to.tv_sec;
|
||||
if ((to.tv_nsec = at->tv_nsec - to.tv_nsec) < 0) {
|
||||
to.tv_sec--;
|
||||
to.tv_nsec += 1000000000;
|
||||
}
|
||||
if (to.tv_sec < 0) return ETIMEDOUT;
|
||||
top = &to;
|
||||
}
|
||||
|
||||
r = -meow_futex4_cp(addr, MEOW_FUTEX_WAIT | priv, val, top);
|
||||
if (r != EINTR && r != ETIMEDOUT && r != ECANCELED) r = 0;
|
||||
if (r == EINTR && !__eintr_valid_flag) r = 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
int __timedwait(volatile int *addr, int val, clockid_t clk,
|
||||
const struct timespec *at, int priv)
|
||||
{
|
||||
int cs, r;
|
||||
__pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs);
|
||||
r = __timedwait_cp(addr, val, clk, at, priv);
|
||||
__pthread_setcancelstate(cs, 0);
|
||||
return r;
|
||||
}
|
||||
|
||||
/* ---- timerfd(musl src/linux/timerfd.c 同款缺陷)----
|
||||
* 回退分支把 32 字节 time64 `struct itimerspec` 直接交给内核;o32 内核只认
|
||||
* 16 字节 {interval.sec, interval.nsec, value.sec, value.nsec}。实际读到的
|
||||
* value.sec = 调用者 interval.tv_nsec(错位),value.nsec = 结构填充(垃圾);
|
||||
* gettime 反向把内核的前 16 字节写进错位的字段。SDK/App 当前未用 timerfd,
|
||||
* 但成本极低,顺手按内核 ABI 转换。三个符号必须同一 TU 全定义:libc 的
|
||||
* timerfd.o 同时提供 create/settime/gettime,缺一个就会把它拉进来导致重复定义。 */
|
||||
int timerfd_create(int clockid, int flags)
|
||||
{
|
||||
return (int)syscall(SYS_timerfd_create, clockid, flags);
|
||||
}
|
||||
|
||||
int __timerfd_settime64(int fd, int flags, const struct itimerspec *new,
|
||||
struct itimerspec *old);
|
||||
int __timerfd_gettime64(int fd, struct itimerspec *cur);
|
||||
|
||||
int __timerfd_settime64(int fd, int flags, const struct itimerspec *new,
|
||||
struct itimerspec *old)
|
||||
{
|
||||
struct meow_time64_itimerspec in;
|
||||
struct meow_itimerspec32 n32, o32;
|
||||
|
||||
if (!new) { errno = EFAULT; return -1; }
|
||||
in.it_interval.tv_sec = new->it_interval.tv_sec;
|
||||
in.it_interval.tv_nsec = new->it_interval.tv_nsec;
|
||||
in.it_value.tv_sec = new->it_value.tv_sec;
|
||||
in.it_value.tv_nsec = new->it_value.tv_nsec;
|
||||
if (meow_time32_itimerspec_from_64(&in, &n32)) { errno = ENOTSUP; return -1; }
|
||||
|
||||
if (syscall(SYS_timerfd_settime32, fd, flags, &n32, old ? &o32 : 0) != 0)
|
||||
return -1;
|
||||
if (old) {
|
||||
old->it_interval.tv_sec = o32.it_interval.tv_sec;
|
||||
old->it_interval.tv_nsec = o32.it_interval.tv_nsec;
|
||||
old->it_value.tv_sec = o32.it_value.tv_sec;
|
||||
old->it_value.tv_nsec = o32.it_value.tv_nsec;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __timerfd_gettime64(int fd, struct itimerspec *cur)
|
||||
{
|
||||
struct meow_itimerspec32 c32;
|
||||
|
||||
if (!cur) { errno = EFAULT; return -1; }
|
||||
if (syscall(SYS_timerfd_gettime32, fd, &c32) != 0) return -1;
|
||||
cur->it_interval.tv_sec = c32.it_interval.tv_sec;
|
||||
cur->it_interval.tv_nsec = c32.it_interval.tv_nsec;
|
||||
cur->it_value.tv_sec = c32.it_value.tv_sec;
|
||||
cur->it_value.tv_nsec = c32.it_value.tv_nsec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- clock_settime(musl src/time/clock_settime.c 同款缺陷)----
|
||||
* 回退分支把 16 字节 time64 结构直接交给 SYS_clock_settime32;内核只写/读 8 字节,
|
||||
* nsec 位永远为 0。绝对时刻超出 int32 时回 ENOTSUP(与 musl time64 路径一致)。 */
|
||||
int __clock_settime64(clockid_t clk, const struct timespec *ts)
|
||||
{
|
||||
struct meow_time32 t;
|
||||
|
||||
if (!ts) { errno = EFAULT; return -1; }
|
||||
if (meow_time32_abs_from_parts((int64_t)ts->tv_sec, (int64_t)ts->tv_nsec, &t)) {
|
||||
errno = ENOTSUP;
|
||||
return -1;
|
||||
}
|
||||
if (syscall(SYS_clock_settime32, clk, &t) != 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
+230
-76
@@ -79,7 +79,7 @@ struct meow_widget {
|
||||
int maxlen; /* W_TEXTFIELD 上限(含 NUL) */
|
||||
meow_ime_mode ime_mode; /* W_TEXTFIELD IME 模式 */
|
||||
int value; /* W_PROGRESS:0-100 / -1=不定; W_BADGE:计数 */
|
||||
int item_h; /* W_LIST:行高/列宽(lazy),默认 44 */
|
||||
int item_h; /* W_LIST:行高/列宽(lazy),默认 44;W_LISTVIEW:显式行高,0=按字体行高派生 */
|
||||
int scroll; /* W_LIST:滚动偏移(lazy) */
|
||||
int horizontal; /* W_LIST:1=横向(LazyRow) */
|
||||
int back_on; /* W_TOPAPPBAR:显示返回箭头 */
|
||||
@@ -116,6 +116,7 @@ struct meow_widget {
|
||||
int show_icon; /* 1=画左图标/缩略图(默认 1) */
|
||||
int show_right;/* 1=画右尾件(默认 1) */
|
||||
char empty_text[64]; /* 空态占位文案(首字符 0=不画) */
|
||||
int container; /* MD3 容器模式(默认 renderer):1=行=surfaceContainer 圆角卡+空隙 (P0) */
|
||||
|
||||
/* §5.5 左滑删除(列表行横向手势:滑动偏移/删除动画/回弹;calloc 归零,安全)。
|
||||
* 手势由 App 在 on_touch_move 喂(swipe_begin/move/end);横向小位移判 swipe、纵向滚动优先;
|
||||
@@ -141,6 +142,12 @@ static uint32_t widget_fg(const meow_widget *w, const meow_theme *th, uint32_t d
|
||||
{ (void)th; return (w->opts_used && w->opts.fg) ? w->opts.fg : def; }
|
||||
static int widget_radius(const meow_widget *w, int def)
|
||||
{ return (w->opts_used && w->opts.radius >= 0) ? w->opts.radius : def; }
|
||||
/* MD3 全形按钮圆角 = 胶囊(全高一半);opts.radius 显式优先(P1 shape 对齐,替代硬编码 10)。 */
|
||||
static int widget_btn_radius(const meow_widget *w)
|
||||
{
|
||||
if (w->opts_used && w->opts.radius >= 0) return w->opts.radius;
|
||||
return MEOW_SHAPE_FULL(w->h);
|
||||
}
|
||||
static int widget_pady(const meow_widget *w)
|
||||
{ return (w->opts_used && w->opts.pad_y > 0) ? w->opts.pad_y : 0; }
|
||||
|
||||
@@ -400,6 +407,10 @@ meow_widget *meow_list_create(int x, int y, int w, int h, int count,
|
||||
t->owner = meow_ui_activity_active();
|
||||
t->state = MEOW_WS_ENABLED;
|
||||
t->count = count; t->src = src; t->ctx = ctx; t->sel = 0;
|
||||
/* M6.37 P1:SDK 默认列表(meow_list_create / meow_lazycolumn_create / meow_lazyrow_create,皆 W_LIST)
|
||||
* 走 MD3 容器模式——每项=surfaceContainer 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,
|
||||
* 与 meow_listview 容器同范式(全局统一)。需扁平的宿主可显式 meow_list_set_container(w,0) 回退。 */
|
||||
t->container = 1;
|
||||
meow_draw_widget_register(t);
|
||||
return t;
|
||||
}
|
||||
@@ -1007,6 +1018,17 @@ int meow_list_tap_at(meow_widget *w, int x, int y)
|
||||
return idx;
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
if (!w || w->type != W_LIST) return -1;
|
||||
w->container = on ? 1 : 0;
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中
|
||||
* ------------------------------------------------------------
|
||||
@@ -1017,6 +1039,11 @@ int meow_list_tap_at(meow_widget *w, int x, int y)
|
||||
#define MEOW_LISTV_ICON 24 /* 图标盒尺寸(px) */
|
||||
#define MEOW_LISTV_PAD 16 /* 行内左右 padding(px) */
|
||||
#define MEOW_LISTV_SUB_GAP 2 /* 标题与副标题间隙(px) */
|
||||
/* MD3 容器模式(P0):行=surfaceContainer 圆角卡 + 行间空隙(去分隔线);卡图元与主界面/
|
||||
* ncm 歌单同手法(main_menu.c:639 / playlist.c)。卡在行高内上下对称内缩(行中心=卡中心)。 */
|
||||
#define MEOW_LISTV_CARD_MARGIN 8 /* 卡横向外边距(px) */
|
||||
#define MEOW_LISTV_CARD_GAP 8 /* 卡纵向间隙(px) */
|
||||
#define MEOW_LISTV_CARD_RADIUS 12 /* 卡圆角(px,12dp) */
|
||||
|
||||
/* meow_ui_img_scaled 弱引用:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支用它绘制图片图标。
|
||||
* 弱引用让「只用内置符号图标、不解码图片」的 App 不被牵连进 meow_ui_img.o/stb_image.o
|
||||
@@ -1024,7 +1051,24 @@ int meow_list_tap_at(meow_widget *w, int x, int y)
|
||||
* 宿主/设备需图片图标的 App 经 meow_ui_img_load_* 强引用 meow_ui_img.o → 此处自动解析。 */
|
||||
extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attribute__((weak));
|
||||
|
||||
static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; }
|
||||
/* 行高:显式 meow_listview_set_item_h 优先(App 自管行几何时按自己算出的值传,如 ncm 70);
|
||||
* 未显式(item_h<=0)→ 按当前主题正文字体行高派生,保证「文本块 ≤ 行高」:
|
||||
* lines=2:max(56, 2*line_h + MEOW_LISTV_SUB_GAP) (20px 字库行高 30 → 62)
|
||||
* lines=1:max(48, line_h)
|
||||
* 旧默认固定 56 在真机 source_han_serif 20px(ascent 24 + descent 6 = 30)下小于双行
|
||||
* 文本块 62 → 居中后 ty = 行顶 - 3,首行(行顶==视口顶)标题被 `ty >= vy0` 裁剪守卫整条
|
||||
* 跳过,只剩副标题(M12 finding / M15 本源修复)。派生值由主题字库实时算出,draw/hit/
|
||||
* max_scroll 三路共用同一函数 → 行几何恒一致;改字库档位/主题后自动跟随。 */
|
||||
static int listv_item_h(const meow_widget *w)
|
||||
{
|
||||
if (w->item_h > 0) return w->item_h;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int line_h = th ? meow_ui_text_height(th->font_body) : 0;
|
||||
if (w->lines == 1)
|
||||
return (line_h > 48) ? line_h : 48;
|
||||
int h = 2 * line_h + MEOW_LISTV_SUB_GAP;
|
||||
return (h > 56) ? h : 56;
|
||||
}
|
||||
|
||||
/* §5.5 虚拟化辅助:当前行数(pool 模式实时查 count_fn;src2 模式用缓存的 w->count)。 */
|
||||
static int listv_count(const meow_widget *w)
|
||||
@@ -1176,9 +1220,12 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
|
||||
t->owner = meow_ui_activity_active();
|
||||
t->state = MEOW_WS_ENABLED;
|
||||
t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx;
|
||||
t->sel = -1; t->item_h = 56; t->scroll = 0; t->swipe_idx = -1;
|
||||
t->sel = -1; t->item_h = 0; t->scroll = 0; t->swipe_idx = -1; /* item_h=0 → 按字体行高派生(见 listv_item_h) */
|
||||
t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE;
|
||||
t->lines = 2; t->show_icon = 1; t->show_right = 1; /* 默认双行富 item + 图标 + 右尾件(src2 布局不变) */
|
||||
t->container = 1; /* M6.37 P1:SDK 默认列表走 MD3 容器模式——行=surfaceContainer 圆角卡 + 空隙 + 去分隔线 + 卡内 tonal 选中,
|
||||
* 让 hello 示例 / files 及一切用 SDK 默认 renderer 的 App 开箱即 MD3(用户 2026-09-04 反馈默认仍扁平)。
|
||||
* 需扁平的宿主可显式 meow_listview_set_container(lv,0) 回退(test_meow_ui_widgets 即用它锁扁平基线)。 */
|
||||
if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) {
|
||||
t->sel_bits_n = (t->count + 31) / 32;
|
||||
t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t));
|
||||
@@ -1195,7 +1242,12 @@ int meow_listview_set_item_h(meow_widget *w, int item)
|
||||
listv_ensure_pool(w); /* 行高变小 → 可见行增多 → 池容量随之调整 */
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
}
|
||||
return w->item_h;
|
||||
return listv_item_h(w); /* 返回当前生效行高(显式值或字体派生值) */
|
||||
}
|
||||
int meow_listview_item_h(meow_widget *w)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) return 0;
|
||||
return listv_item_h(w);
|
||||
}
|
||||
void meow_listview_set_count(meow_widget *w, int count)
|
||||
{
|
||||
@@ -1368,7 +1420,7 @@ int meow_listview_set_item_renderer(meow_widget *w, int lines, int show_icon,
|
||||
if (lines < 1) lines = 1;
|
||||
if (lines > 2) lines = 2;
|
||||
w->lines = lines; w->show_icon = show_icon ? 1 : 0; w->show_right = show_right ? 1 : 0;
|
||||
if (w->item_h <= 0) w->item_h = (lines == 1) ? 48 : 56; /* 未显式定行高 → 档位默认 */
|
||||
/* 未显式定行高(item_h<=0)→ 保持派生模式(按 lines 档位从字体行高算),不写死像素 */
|
||||
listv_ensure_pool(w); /* 行档位可能变化 → 确保池容量 */
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
return 0;
|
||||
@@ -1380,7 +1432,7 @@ int meow_listview_set_item_tier(meow_widget *w, int lines)
|
||||
if (lines < 1) lines = 1;
|
||||
if (lines > 2) lines = 2;
|
||||
w->lines = lines;
|
||||
w->item_h = (lines == 1) ? 48 : 56; /* 档位默认高(可随后 set_item_h 覆盖) */
|
||||
w->item_h = 0; /* 档位=字体派生默认(单行 max(48,行高) / 双行 max(56,2*行高+2));set_item_h 可覆盖 */
|
||||
listv_ensure_pool(w);
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
return 0;
|
||||
@@ -1395,6 +1447,14 @@ int meow_listview_set_item_draw(meow_widget *w, meow_listview_draw_fn draw,
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_listview_set_container(meow_widget *w, int on)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) return -1;
|
||||
w->container = on ? 1 : 0;
|
||||
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_listview_set_empty_text(meow_widget *w, const char *text)
|
||||
{
|
||||
if (!w || w->type != W_LISTVIEW) return -1;
|
||||
@@ -1631,7 +1691,7 @@ void meow_widget_draw(meow_widget *w)
|
||||
uint32_t bg;
|
||||
if (fill) bg = (hold) ? state_tint(base, th, 24) : (press ? state_tint(base, th, 12) : base);
|
||||
else bg = 0;
|
||||
int r = widget_radius(w, 10);
|
||||
int r = widget_btn_radius(w);
|
||||
if (fill) meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, bg);
|
||||
else if (press) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->primary, 12); /* transparent ripple */
|
||||
/* 内容区受 pad 垂直内缩(水平居中文本,pad_x 不影响中心) */
|
||||
@@ -1693,17 +1753,25 @@ void meow_widget_draw(meow_widget *w)
|
||||
break;
|
||||
}
|
||||
case W_SWITCH: {
|
||||
uint32_t bg = w->on ? th->primary : th->onSurfaceVariant;
|
||||
/* MD3(P3):on-track=primary;off-track=surfaceContainerHighest(原 onSurfaceVariant 过暗);
|
||||
* on-thumb=onPrimary;off-thumb=surface + outline(原恒白)。 */
|
||||
uint32_t bg = w->on ? th->primary : th->surfaceContainerHighest;
|
||||
meow_gfx_fill_round(w->x, w->y, w->w, w->h, w->h / 2, bg);
|
||||
int knob_r = w->h / 2 - 3;
|
||||
int kx = w->on ? w->x + w->w - knob_r * 2 - 3 : w->x + 3;
|
||||
meow_gfx_fill_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, 0xFFFFFF);
|
||||
uint32_t knob = w->on ? th->onPrimary : th->surface; /* off-thumb 表面色 */
|
||||
meow_gfx_fill_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, knob);
|
||||
if (!w->on) meow_ui_draw_rect_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, th->outline);
|
||||
if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->h / 2, th->onSurface, 90);
|
||||
break;
|
||||
}
|
||||
case W_LIST: {
|
||||
/* §5.8.6 列表容器(LazyColumn/LazyRow):只画可见项(虚拟化)+ 滚动偏移。
|
||||
* 纵向=行高 item_h(默认 44,兼容既有 meow_list_create);横向=LazyRow(列宽=item_h)。 */
|
||||
* 纵向=行高 item_h(默认 44,兼容既有 meow_list_create);横向=LazyRow(列宽=item_h)。
|
||||
* MD3 容器模式(w->container=1,默认):每项外套 surfaceContainer 圆角卡(12dp)+ 项间空隙
|
||||
* (去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal),与 meow_listview 容器同范式;
|
||||
* 卡在项内交叉轴内缩 MEOW_LISTV_CARD_MARGIN、滚动轴对称内缩 MEOW_LISTV_CARD_GAP(行中心=卡中心)。
|
||||
* 非容器(meow_list_set_container(0) 回退):满幅扁平行 + primary 选中 + onSurfaceVariant 分隔线。 */
|
||||
int pad = 16;
|
||||
int item = list_item(w);
|
||||
int vp = w->horizontal ? w->w : w->h;
|
||||
@@ -1720,36 +1788,70 @@ void meow_widget_draw(meow_widget *w)
|
||||
* 画到视口外 → 标脏并上屏 → 标题栏/底部残留脏区,P3)。 */
|
||||
int vx0 = w->x, vy0 = w->y, vx1 = w->x + w->w, vy1 = w->y + w->h;
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
int cm = w->container ? MEOW_LISTV_CARD_MARGIN : 0; /* 卡交叉轴内缩(纵向=水平→横向=垂直) */
|
||||
int cgap = w->container ? MEOW_LISTV_CARD_GAP : 0; /* 卡滚动轴对称内缩(去分隔线、留空隙) */
|
||||
for (int i = first; i < last; i++) {
|
||||
char label[64] = "";
|
||||
if (!w->src || !w->src(i, label, (int)sizeof label, w->ctx)) continue;
|
||||
int pos = i * item - off; /* 相对容器起点的可视位置 */
|
||||
int sel = (i == w->sel);
|
||||
if (w->horizontal) {
|
||||
int rx = w->x + pos;
|
||||
if (rx >= vx1) break; /* 完全在右缘外 */
|
||||
if (rx + item <= vx0) continue; /* 完全在左缘外(滚出残余) */
|
||||
int cl = rx > vx0 ? rx : vx0; /* 可见左界 */
|
||||
int cr = (rx + item) < vx1 ? (rx + item) : vx1; /* 可见右界 */
|
||||
if (i == w->sel) meow_ui_fill_rect(cl, w->y, cr - cl, w->h, th->primary);
|
||||
uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface;
|
||||
int ctop = w->y + cm; /* 卡顶(交叉轴内缩) */
|
||||
int chh = w->h - 2 * cm; /* 卡高 */
|
||||
int clft = rx + cgap / 2; /* 卡左(滚动轴对称内缩) */
|
||||
int cwid = item - cgap; /* 卡宽 */
|
||||
int press = meow_input_touch_down() &&
|
||||
meow_input_touch_x() >= rx && meow_input_touch_x() < rx + item &&
|
||||
meow_input_touch_y() >= w->y && meow_input_touch_y() < w->y + w->h;
|
||||
if (w->container) {
|
||||
meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
|
||||
if (sel) meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
|
||||
else if (press) meow_gfx_fill_round_alpha(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
|
||||
} else {
|
||||
if (sel) meow_ui_fill_rect(cl, w->y, cr - cl, w->h, th->primary);
|
||||
}
|
||||
uint32_t col = sel ? (w->container ? th->onPrimaryContainer : 0xFFFFFF) : th->onSurface;
|
||||
int ty = w->y + (w->h - thh) / 2;
|
||||
if (rx >= vx0 && rx + item <= vx1) /* 仅整项可见才画文本(防泄漏) */
|
||||
meow_ui_text_center(rx + item / 2, ty, label, th->font_body, col);
|
||||
int sxp = rx + item - 1; /* 分隔线:仅当落在可见区 */
|
||||
if (sxp >= cl && sxp < cr) meow_ui_fill_rect(sxp, w->y, 1, w->h, th->onSurfaceVariant);
|
||||
if (!w->container) { /* 非容器才画分隔线(容器模式去冗余) */
|
||||
int sxp = rx + item - 1; /* 分隔线:仅当落在可见区 */
|
||||
if (sxp >= cl && sxp < cr) meow_ui_fill_rect(sxp, w->y, 1, w->h, th->onSurfaceVariant);
|
||||
}
|
||||
} else {
|
||||
int ry = w->y + pos;
|
||||
if (ry >= vy1) break; /* 完全在底缘外 */
|
||||
if (ry + item <= vy0) continue; /* 完全在顶缘外(滚出残余) */
|
||||
int cl = ry > vy0 ? ry : vy0; /* 可见顶界 */
|
||||
int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */
|
||||
if (i == w->sel) meow_ui_fill_rect(w->x, cl, w->w, cr - cl, th->primary);
|
||||
uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface;
|
||||
int clft = w->x + cm; /* 卡左(交叉轴内缩) */
|
||||
int cwid = w->w - 2 * cm; /* 卡宽 */
|
||||
int ctop = ry + cgap / 2; /* 卡顶(滚动轴对称内缩) */
|
||||
int chh = item - cgap; /* 卡高 */
|
||||
int press = meow_input_touch_down() &&
|
||||
meow_input_touch_x() >= w->x && meow_input_touch_x() < w->x + w->w &&
|
||||
meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item;
|
||||
if (w->container) {
|
||||
meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
|
||||
if (sel) meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
|
||||
else if (press) meow_gfx_fill_round_alpha(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
|
||||
} else {
|
||||
if (sel) meow_ui_fill_rect(w->x, cl, w->w, cr - cl, th->primary);
|
||||
}
|
||||
uint32_t col = sel ? (w->container ? th->onPrimaryContainer : 0xFFFFFF) : th->onSurface;
|
||||
int txx = (w->container ? clft : w->x) + pad; /* 卡内文本左缘 */
|
||||
int ty = ry + (item - thh) / 2;
|
||||
if (ty >= vy0 && ty + thh <= vy1) /* 文本不越界才画(防标题/底部泄漏) */
|
||||
meow_ui_text(w->x + pad, ty, label, th->font_body, col);
|
||||
int sy = ry + item - 1; /* 分隔线:仅当落在可见区 */
|
||||
if (sy >= cl && sy < cr) meow_ui_fill_rect(w->x, sy, w->w, 1, th->onSurfaceVariant);
|
||||
meow_ui_text(txx, ty, label, th->font_body, col);
|
||||
if (!w->container) { /* 非容器才画分隔线(容器模式去冗余) */
|
||||
int sy = ry + item - 1; /* 分隔线:仅当落在可见区 */
|
||||
if (sy >= cl && sy < cr) meow_ui_fill_rect(w->x, sy, w->w, 1, th->onSurfaceVariant);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
|
||||
@@ -1777,11 +1879,16 @@ void meow_widget_draw(meow_widget *w)
|
||||
int vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h;
|
||||
int thh = meow_ui_text_height(th->font_body);
|
||||
int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON;
|
||||
int text_x = vx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */
|
||||
int op_start = (w->show_right) ? (vx0 + (w->w * 2 / 3)) /* 行右 1/3 = 操作区 */
|
||||
: (vx0 + w->w - pad);
|
||||
/* 容器模式(P0):行=surfaceContainer 圆角卡,卡横向内缩 cm;内容左缘/操作区/可用宽
|
||||
* 都在卡内计算(非容器 cm=0 → 与既有行为逐字一致)。 */
|
||||
int cm = w->container ? MEOW_LISTV_CARD_MARGIN : 0;
|
||||
int cw = w->w - 2 * cm; /* 内容/卡宽 */
|
||||
int cx0 = vx0 + cm; /* 内容左缘(卡内) */
|
||||
int text_x = cx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */
|
||||
int op_start = (w->show_right) ? (cx0 + (cw * 2 / 3)) /* 行右 1/3 = 操作区 */
|
||||
: (cx0 + cw - pad);
|
||||
int text_maxw = (w->show_right) ? (op_start - pad - text_x) /* 标题/副标题可用宽(不压操作区) */
|
||||
: ((vx0 + w->w - pad) - text_x);
|
||||
: ((cx0 + cw - pad) - text_x);
|
||||
if (text_maxw < 8) text_maxw = 8;
|
||||
|
||||
/* 空态占位(§5.5):count==0 且设了 empty_text → 视口内居中画文案。 */
|
||||
@@ -1841,26 +1948,36 @@ void meow_widget_draw(meow_widget *w)
|
||||
meow_str("common.delete"), th->font_body, th->onError);
|
||||
shift = w->swipe_dx;
|
||||
}
|
||||
int rlx = vx0 + shift; /* 行内容左移后的 x */
|
||||
int rlx = (w->container ? cx0 : vx0) + shift; /* 行内容/卡左缘(随行左移) */
|
||||
int rtx = text_x + shift; /* 文本 x(随行左移) */
|
||||
int rop = op_start + shift; /* 操作区分界 x */
|
||||
int card_y = ry + MEOW_LISTV_CARD_GAP / 2; /* 容器模式:卡顶(行内对称内缩) */
|
||||
int card_h = item - MEOW_LISTV_CARD_GAP;
|
||||
|
||||
if (it->section) { /* 段头占位行(非粘性):surfaceContainerHigh 底 + primary 段名 */
|
||||
meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->surfaceContainerHigh);
|
||||
if (it->section) { /* 段头占位行(非粘性):容器=surface 上的 primary 段标签;非容器=surfaceContainerHigh 底 */
|
||||
if (!w->container) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->surfaceContainerHigh);
|
||||
int sty = ry + (item - thh) / 2;
|
||||
if (it->title && sty >= vy0 && sty + thh <= vy1)
|
||||
listv_text_trunc(rtx, sty, it->title, text_maxw, th->font_body, th->primary);
|
||||
int sy = ry + item - 1;
|
||||
if (sy >= cl && sy < cr)
|
||||
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
|
||||
if (!it->enabled)
|
||||
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
|
||||
if (!w->container) {
|
||||
int sy = ry + item - 1;
|
||||
if (sy >= cl && sy < cr)
|
||||
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
|
||||
if (!it->enabled)
|
||||
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 选中/按压背景(primaryContainer / 半透明主题色按压层) */
|
||||
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
|
||||
else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26);
|
||||
/* 选中/按压背景(容器=卡内 tonal;非容器=满幅实心,原行为) */
|
||||
if (w->container) {
|
||||
meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
|
||||
if (sel) meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
|
||||
else if (press) meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
|
||||
} else {
|
||||
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
|
||||
else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26);
|
||||
}
|
||||
uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface;
|
||||
uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
|
||||
|
||||
@@ -1890,24 +2007,31 @@ void meow_widget_draw(meow_widget *w)
|
||||
|
||||
/* 右尾件(show_right=1:箭头/开关/复选/⋯) */
|
||||
if (w->show_right && it->right != MEOW_LIST_RIGHT_NONE) {
|
||||
int ax = rlx + w->w - pad - 24; /* 右缘动作槽(约 24px) */
|
||||
int ax = (cx0 + cw - pad - 24) + shift; /* 右缘动作槽(约 24px;容器=卡内右缘) */
|
||||
if (ax >= rop)
|
||||
listv_draw_right(th, ax, ry, item, it->right, it->on, subcol);
|
||||
}
|
||||
|
||||
/* 分隔线(仅落在可见带内才画) */
|
||||
int sy = ry + item - 1;
|
||||
if (sy >= cl && sy < cr)
|
||||
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
|
||||
/* 分隔线(仅非容器画;容器模式去冗余分隔线,行间用空隙替代) */
|
||||
if (!w->container) {
|
||||
int sy = ry + item - 1;
|
||||
if (sy >= cl && sy < cr)
|
||||
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
|
||||
}
|
||||
|
||||
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */
|
||||
if (!it->enabled)
|
||||
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
|
||||
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗;容器=卡内淡) */
|
||||
if (!it->enabled) {
|
||||
if (w->container)
|
||||
meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->onSurface, 90);
|
||||
else
|
||||
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* ① src2 模式(既有):每帧按需取行(meow_list2_source),行为不变。
|
||||
* 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。 */
|
||||
/* ① src2 模式(既有):每帧按需取行(meow_list2_source),虚拟化行为不变。
|
||||
* 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer。
|
||||
* lines=1(set_item_renderer/tier 单行档位)时不画副标题,与 pool 分支同口径。 */
|
||||
for (int i = first; i < last; i++) {
|
||||
meow_list_item it;
|
||||
memset(&it, 0, sizeof it);
|
||||
@@ -1918,10 +2042,13 @@ void meow_widget_draw(meow_widget *w)
|
||||
int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */
|
||||
int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */
|
||||
int sel = (i == w->sel);
|
||||
int press = meow_input_touch_down() &&
|
||||
meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w &&
|
||||
meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item;
|
||||
uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface;
|
||||
uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
|
||||
/* 左滑删除(M7.3):本行被滑动 → 右露删除区 + 行 x 左移。非滑动行 shift=0 → 输出不变。 */
|
||||
int shift = 0, rlx = vx0, rtx = text_x, rop = op_start;
|
||||
int shift = 0, rlx = (w->container ? cx0 : vx0), rtx = text_x, rop = op_start;
|
||||
if (w->swipe_active && w->swipe_idx == i && w->swipe_dx < 0) {
|
||||
meow_ui_fill_rect(vx0 + w->w + w->swipe_dx, ry, -w->swipe_dx, item, th->error);
|
||||
int thh2 = meow_ui_text_height(th->font_body);
|
||||
@@ -1929,9 +2056,19 @@ void meow_widget_draw(meow_widget *w)
|
||||
if (dly >= vy0 && dly + thh2 <= vy1)
|
||||
meow_ui_text_center(vx0 + w->w + w->swipe_dx + (-w->swipe_dx) / 2, dly,
|
||||
meow_str("common.delete"), th->font_body, th->onError);
|
||||
shift = w->swipe_dx; rlx = vx0 + shift; rtx = text_x + shift; rop = op_start + shift;
|
||||
shift = w->swipe_dx; rlx = (w->container ? cx0 : vx0) + shift;
|
||||
rtx = text_x + shift; rop = op_start + shift;
|
||||
}
|
||||
int card_y = ry + MEOW_LISTV_CARD_GAP / 2;
|
||||
int card_h = item - MEOW_LISTV_CARD_GAP;
|
||||
if (w->container) {
|
||||
meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
|
||||
if (sel) meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
|
||||
else if (press) meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
|
||||
} else {
|
||||
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
|
||||
else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26);
|
||||
}
|
||||
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
|
||||
|
||||
/* 图标(左) */
|
||||
uint32_t iccol = sel ? th->onPrimaryContainer
|
||||
@@ -1941,9 +2078,11 @@ void meow_widget_draw(meow_widget *w)
|
||||
if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */
|
||||
listv_draw_icon(th, rlx + pad, icy, it.icon_kind, it.icon, iccol);
|
||||
|
||||
/* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */
|
||||
/* 标题 + 副标题(lines=2 有副标题→两行居中;lines=1 或无副标题→单行居中;越界不画防泄漏)。
|
||||
* lines 档位与 pool 分支同口径:tier/renderer 设单行时明确不画副标题,
|
||||
* 否则 48px 单行档位里塞 62px 文本块 → 首行标题被裁剪守卫跳过。 */
|
||||
int ty;
|
||||
if (it.subtitle && it.subtitle[0]) {
|
||||
if (w->lines == 2 && it.subtitle && it.subtitle[0]) {
|
||||
ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2;
|
||||
if (it.title && ty >= vy0 && ty + thh <= vy1)
|
||||
listv_text_trunc(rtx, ty, it.title, text_maxw, th->font_body, textcol);
|
||||
@@ -1958,19 +2097,25 @@ void meow_widget_draw(meow_widget *w)
|
||||
|
||||
/* 右侧操作区 */
|
||||
if (it.right != MEOW_LIST_RIGHT_NONE) {
|
||||
int ax = rlx + w->w - pad - 24; /* 右缘动作槽(约 24px) */
|
||||
int ax = (cx0 + cw - pad - 24) + shift; /* 右缘动作槽(约 24px;容器=卡内右缘) */
|
||||
if (ax >= rop)
|
||||
listv_draw_right(th, ax, ry, item, it.right, it.on, subcol);
|
||||
}
|
||||
|
||||
/* 分隔线(仅落在可见带内才画) */
|
||||
int sy = ry + item - 1;
|
||||
if (sy >= cl && sy < cr)
|
||||
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
|
||||
/* 分隔线(仅非容器画;容器模式去冗余分隔线,行间用空隙替代) */
|
||||
if (!w->container) {
|
||||
int sy = ry + item - 1;
|
||||
if (sy >= cl && sy < cr)
|
||||
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
|
||||
}
|
||||
|
||||
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */
|
||||
if (!it.enabled)
|
||||
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
|
||||
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗;容器=卡内淡) */
|
||||
if (!it.enabled) {
|
||||
if (w->container)
|
||||
meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->onSurface, 90);
|
||||
else
|
||||
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
|
||||
@@ -1988,7 +2133,7 @@ void meow_widget_draw(meow_widget *w)
|
||||
break;
|
||||
case W_TEXTFIELD: {
|
||||
/* MD3 输入框:圆角外框(surfaceContainerLowest 底 + outline 描边;focused=primary 2px)+ 提示/文本 + 光标。 */
|
||||
int r = widget_radius(w, 8);
|
||||
int r = widget_radius(w, MEOW_SHAPE_SMALL);
|
||||
int focused = (w->state & MEOW_WS_FOCUSED) != 0;
|
||||
meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, th->surfaceContainerLowest);
|
||||
uint32_t bcol = focused ? th->primary : th->outline;
|
||||
@@ -2045,55 +2190,63 @@ void meow_widget_draw(meow_widget *w)
|
||||
break;
|
||||
}
|
||||
case W_CHECKBOX: {
|
||||
/* MD3(P3):视觉字 24dp 于控件矩形内居中(触摸目标不变);修正原 34 视觉过大。 */
|
||||
int sel = (w->state & MEOW_WS_SELECTED) != 0;
|
||||
int sz = w->w;
|
||||
int r = widget_radius(w, 4);
|
||||
int sz = 24;
|
||||
if (w->w < sz) sz = w->w; /* 控件小于视觉尺寸时跟随 */
|
||||
int bx = w->x + (w->w - sz) / 2, by = w->y + (w->h - sz) / 2;
|
||||
int r = widget_radius(w, MEOW_SHAPE_EXTRASMALL);
|
||||
if (sel) {
|
||||
meow_gfx_fill_round(w->x, w->y, sz, sz, r, th->primary);
|
||||
int ax = w->x + 9, ay1 = w->y + 17, ay2 = w->y + 23, bx = w->x + 15, cx = w->x + 25, cy = w->y + 11;
|
||||
meow_gfx_line(ax, ay1, bx, ay2, 3, th->onPrimary);
|
||||
meow_gfx_line(bx, ay2, cx, cy, 3, th->onPrimary);
|
||||
meow_gfx_fill_round(bx, by, sz, sz, r, th->primary);
|
||||
int ax = bx + 7, ay1 = by + 13, ay2 = by + 17, b2 = bx + 11, cx = bx + 18, cy = by + 7;
|
||||
meow_gfx_line(ax, ay1, b2, ay2, 2, th->onPrimary);
|
||||
meow_gfx_line(b2, ay2, cx, cy, 2, th->onPrimary);
|
||||
} else {
|
||||
meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->outline);
|
||||
meow_ui_draw_rect_round(bx, by, sz, sz, r, th->outline);
|
||||
}
|
||||
if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->primary);
|
||||
if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, sz, sz, r, th->onSurface, 90);
|
||||
if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(bx, by, sz, sz, r, th->primary);
|
||||
if (disabled) meow_gfx_fill_round_alpha(bx, by, sz, sz, r, th->onSurface, 90);
|
||||
break;
|
||||
}
|
||||
case W_RADIO: {
|
||||
/* MD3(P3):视觉 20dp(环半径 10)于控件内居中;原环半径 12 略大。 */
|
||||
int sel = (w->state & MEOW_WS_SELECTED) != 0;
|
||||
int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2;
|
||||
meow_gfx_ring_arc(ccx, ccy, 12, 2, 0, 360, sel ? th->primary : th->outline);
|
||||
if (sel) meow_gfx_fill_circle(ccx, ccy, 6, th->primary);
|
||||
meow_gfx_ring_arc(ccx, ccy, 10, 2, 0, 360, sel ? th->primary : th->outline);
|
||||
if (sel) meow_gfx_fill_circle(ccx, ccy, 5, th->primary);
|
||||
if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->w / 2, th->onSurface, 90);
|
||||
break;
|
||||
}
|
||||
case W_PROGRESS: {
|
||||
/* 批5b:轨道/填充可由 opts 覆盖(opts.bg=轨道、opts.fg=填充);opts 未设则回主题默认
|
||||
* (surfaceVariant/primary)——兼容既有调用(如 hello demo、现在播放页取色后回退默认)。 */
|
||||
uint32_t track = widget_bg(w, th, th->surfaceVariant);
|
||||
uint32_t fill = widget_fg(w, th, th->primary);
|
||||
if (w->variant == 1) { /* 圆形(几何 = 整圆像素外接框;由此解回圆心/半径) */
|
||||
int thk = MEOW_PROGRESS_ARC_THK;
|
||||
int m = MEOW_PROGRESS_ARC_MARGIN;
|
||||
int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2;
|
||||
int rr = w->w / 2 - m; /* 与 create 一致:回到调用方传入的半径 r */
|
||||
if (rr <= 0) break;
|
||||
meow_gfx_ring_arc(ccx, ccy, rr, thk, 0, 360, th->surfaceVariant);
|
||||
meow_gfx_ring_arc(ccx, ccy, rr, thk, 0, 360, track);
|
||||
if (w->value < 0) { /* 不定:按时间推进一段弧(App 在 on_tick 里重绘驱动) */
|
||||
int start = (int)((meow_ui_time_ms() / 30) % 360);
|
||||
meow_gfx_ring_arc(ccx, ccy, rr, thk, start - 90, 90, th->primary);
|
||||
meow_gfx_ring_arc(ccx, ccy, rr, thk, start - 90, 90, fill);
|
||||
} else if (w->value > 0) {
|
||||
int sweep = w->value * 360 / 100;
|
||||
if (sweep > 0) meow_gfx_ring_arc(ccx, ccy, rr, thk, -90, sweep, th->primary);
|
||||
if (sweep > 0) meow_gfx_ring_arc(ccx, ccy, rr, thk, -90, sweep, fill);
|
||||
}
|
||||
} else { /* 线性 */
|
||||
int thk = w->h > 0 ? w->h : 4;
|
||||
int yy = w->y + w->h / 2 - thk / 2;
|
||||
meow_ui_fill_rect(w->x, yy, w->w, thk, th->surfaceVariant);
|
||||
meow_ui_fill_rect(w->x, yy, w->w, thk, track);
|
||||
if (w->value < 0) { /* 不定:移动块 */
|
||||
int fw = w->w / 3;
|
||||
int pos = (int)((meow_ui_time_ms() / 12) % (w->w + fw)) - fw;
|
||||
meow_ui_fill_rect(w->x + pos, yy, fw, thk, th->primary);
|
||||
meow_ui_fill_rect(w->x + pos, yy, fw, thk, fill);
|
||||
} else if (w->value > 0) {
|
||||
int fw = w->value * w->w / 100;
|
||||
if (fw > 0) meow_ui_fill_rect(w->x, yy, fw, thk, th->primary);
|
||||
if (fw > 0) meow_ui_fill_rect(w->x, yy, fw, thk, fill);
|
||||
}
|
||||
}
|
||||
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
|
||||
@@ -2150,7 +2303,8 @@ void meow_widget_draw(meow_widget *w)
|
||||
for (int i = 0; i < w->nitems; i++) {
|
||||
int ix = w->x + i * iw;
|
||||
int sel = (i == w->selected);
|
||||
if (sel) meow_gfx_fill_round(ix + iw / 2 - 24, w->y + 8, 48, 22, 11, th->secondaryContainer);
|
||||
if (sel) /* MD3 active pill 64×32(原 48×22 偏小);垂直居中于栏 */
|
||||
meow_gfx_fill_round(ix + iw / 2 - 32, w->y + (w->h - 32) / 2, 64, 32, 16, th->secondaryContainer);
|
||||
uint32_t col = sel ? th->onSecondaryContainer : th->onSurfaceVariant;
|
||||
meow_ui_text_center(ix + iw / 2, ay, w->items[i], th->font_body, col);
|
||||
}
|
||||
|
||||
+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);
|
||||
|
||||
+42
-7
@@ -3,14 +3,18 @@
|
||||
* ② K_ENTER 只提交不关(meow_ime_active() 仍真),ESC 后关;
|
||||
* ③ 阻塞 IME 循环喂 on_tick(动态内容重绘)且 IME 面板仍在最上
|
||||
* (On_tick 在 IME 面板区画红色哨兵行,IME 绘制后应盖回面板色,不被 App 覆盖)。
|
||||
* ④ bug3(2026-09-15):IME 打开 ~200ms TAP 冷却窗——打开后立即注入的 stray-TAP
|
||||
* (背景/键区)被吞不取消;冷却窗后背景 TAP=取消语义保留。
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \
|
||||
* meow_ui_anim.c meow_ui_media.c meow_ui_nav.c meow_pinyin.c meow_ui_box.c meow_ui_scroll.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
|
||||
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm;
|
||||
* widget/input/ime/view 后续又引入 anim/media/nav/pinyin/box/scroll 依赖,一并链接)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
@@ -54,6 +58,38 @@ static int near(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14
|
||||
/* ---- ① 异步 shift 测试(直接 handle_tap,确定语义,无阻塞循环) ---- */
|
||||
static char g_echo[64] = "";
|
||||
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
|
||||
|
||||
/* ④ WiFi 设置页 bug3(2026-09-15):IME 打开 ~200ms TAP 冷却窗——触发打开那次点按的迟到
|
||||
* 「抬起 TAP」(goodix 长尾,经 SDK 输入线程 drain 后才入队)不得把键盘当「点背景=取消」
|
||||
* 瞬间关掉;冷却窗后点背景=取消的语义保留。 */
|
||||
static void test_stray_tap(void)
|
||||
{
|
||||
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
||||
c.mode = MEOW_IME_ASCII; c.maxlen = 32; c.on_change = ime_cb;
|
||||
meow_ui_text_input_async(&c);
|
||||
assert(meow_ime_active() == 1);
|
||||
|
||||
/* 打开后立即注入背景 TAP(y<KB_TOP=420):冷却窗内必须被吞——不取消、键盘仍 active */
|
||||
meow_input_ev stray = { MEOW_EV_TAP, 240, 100 };
|
||||
assert(meow_ime_handle_event(&stray) == 1); /* 消费(吞掉) */
|
||||
assert(meow_ime_active() == 1); /* 键盘未被关(修复前此处已 done=取消) */
|
||||
|
||||
/* 窗口内键区 TAP 也丢弃(防幻影按键) */
|
||||
meow_input_ev key = { MEOW_EV_TAP, K_Q_X, K_Q_Y };
|
||||
assert(meow_ime_handle_event(&key) == 1);
|
||||
assert(meow_ime_active() == 1);
|
||||
assert(meow_ime_text()[0] == 0); /* 无幻影字符入缓冲 */
|
||||
|
||||
/* 冷却窗(200ms)后:点背景=取消 语义保留 */
|
||||
usleep(250000);
|
||||
assert(meow_ime_handle_event(&stray) == 1);
|
||||
assert(meow_ime_active() == 0); /* 已取消(g_ime_done) */
|
||||
printf("PASS stray-tap: 窗内背景/键区 TAP 吞掉不取消, 窗后背景 TAP 取消\n"); fflush(stdout);
|
||||
|
||||
meow_ime_close(0); /* 复位(清 g_ime_done/active) */
|
||||
assert(meow_ime_active() == 0);
|
||||
}
|
||||
|
||||
static void test_shift(void)
|
||||
{
|
||||
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
||||
@@ -94,8 +130,6 @@ static char g_drv_text[64] = ""; /* shift 序列后 meow_ime_text()(期
|
||||
/* 把 back_buf 写成 PPM(视觉目检 IME 面板渲染/最上;在 IME 空闲时调用,back_buf 稳定) */
|
||||
static void dump_ppm(const char *path)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
|
||||
int W = meow_ui_width(), H = meow_ui_height();
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return;
|
||||
@@ -134,7 +168,7 @@ static void *driver(void *arg)
|
||||
(void)arg;
|
||||
usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */
|
||||
meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */
|
||||
usleep(160000);
|
||||
usleep(350000); /* 等过 IME 打开 TAP 冷却窗(200ms,bug3)+ drain 40ms */
|
||||
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */
|
||||
usleep(90000);
|
||||
meow_input_inject(MEOW_EV_TAP, K_SHIFT_X, K_SHIFT_Y); /* shift 开 */
|
||||
@@ -169,6 +203,7 @@ int main(void)
|
||||
const meow_theme *th = meow_theme_get();
|
||||
|
||||
test_shift(); /* ① */
|
||||
test_stray_tap(); /* ④ bug3:打开冷却窗吞 stray-TAP */
|
||||
|
||||
/* ②③ 在 meow_ui_run 里驱动(g_events 仅 meow_ui_run 能设置) */
|
||||
g_tf = meow_textfield_create(10, 300, 300, 44, "Type here...", g_tfbuf, 32, MEOW_IME_ASCII);
|
||||
|
||||
@@ -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -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,170 @@
|
||||
/* test_meow_time64.c — 宿主单测:time64 → 内核 o32 time32 的换算与结构宽度。
|
||||
*
|
||||
* 背景(M16):zig clang 给 32 位 mips 预定义 __ILP32__,打穿 musl 的 time64 回退守卫,
|
||||
* futex 相对超时 / timerfd / clock_settime 等入口把 16 字节(time64)结构直接交给 time32
|
||||
* 系统调用。meow_ui_time.c 的接管入口统一经 meow_ui_internal.h 里这几个纯换算函数转换,
|
||||
* 本测试在宿主上把「换算结果 + 内核实际读到的宽度」固定住,不需要 mips 设备。
|
||||
*
|
||||
* 编译(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_time64.c /tmp/meow_ui_time.o \
|
||||
* -o test_meow_time64
|
||||
* 运行:./test_meow_time64 (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
static int g_fail = 0;
|
||||
|
||||
static void check(const char *name, int ok)
|
||||
{
|
||||
if (ok) {
|
||||
printf("ok %s\n", name);
|
||||
} else {
|
||||
printf("FAIL %s\n", name);
|
||||
g_fail = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void check_time32(const char *name, const struct meow_time32 *got,
|
||||
int32_t sec, int32_t nsec)
|
||||
{
|
||||
if (got->tv_sec == sec && got->tv_nsec == nsec) {
|
||||
printf("ok %s = {%d, %d}\n", name, (int)got->tv_sec, (int)got->tv_nsec);
|
||||
} else {
|
||||
printf("FAIL %s = {%d, %d},期望 {%d, %d}\n", name,
|
||||
(int)got->tv_sec, (int)got->tv_nsec, (int)sec, (int)nsec);
|
||||
g_fail = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 模拟 o32 内核对「调用者给的指针」的实际读法:只读前 8 字节,按两个 int32 解释。 */
|
||||
static void kernel_time32_view(const void *ptr, struct meow_time32 *out)
|
||||
{
|
||||
memcpy(out, ptr, sizeof *out);
|
||||
}
|
||||
|
||||
static void test_rel(void)
|
||||
{
|
||||
struct meow_time32 t;
|
||||
|
||||
meow_time32_rel_from_parts(5, 123456789, &t);
|
||||
check_time32("rel 普通值", &t, 5, 123456789);
|
||||
|
||||
meow_time32_rel_from_parts(0, 500000000, &t);
|
||||
check_time32("rel 亚秒(原缺陷会变 {0,0})", &t, 0, 500000000);
|
||||
|
||||
meow_time32_rel_from_parts(INT32_MAX, 999999999, &t);
|
||||
check_time32("rel int32 上界", &t, INT32_MAX, 999999999);
|
||||
|
||||
meow_time32_rel_from_parts((int64_t)INT32_MAX + 1, 1, &t);
|
||||
check_time32("rel 上溢饱和到 INT32_MAX", &t, INT32_MAX, 1);
|
||||
|
||||
meow_time32_rel_from_parts(INT64_MAX, 0, &t);
|
||||
check_time32("rel INT64_MAX 饱和", &t, INT32_MAX, 0);
|
||||
|
||||
meow_time32_rel_from_parts(-1, 500000000, &t);
|
||||
check_time32("rel 负值透传(调用方另判 ETIMEDOUT)", &t, -1, 500000000);
|
||||
|
||||
meow_time32_rel_from_parts((int64_t)INT32_MIN - 1, 0, &t);
|
||||
check_time32("rel 下溢饱和到 INT32_MIN", &t, INT32_MIN, 0);
|
||||
}
|
||||
|
||||
static void test_abs(void)
|
||||
{
|
||||
struct meow_time32 t;
|
||||
|
||||
t.tv_sec = 7;
|
||||
t.tv_nsec = 7;
|
||||
check("abs 普通值", meow_time32_abs_from_parts(1700000000, 250000000, &t) == 0);
|
||||
check_time32("abs 普通值内容", &t, 1700000000, 250000000);
|
||||
|
||||
check("abs int32 上界接受", meow_time32_abs_from_parts(INT32_MAX, 0, &t) == 0);
|
||||
check("abs int32+1 拒绝", meow_time32_abs_from_parts((int64_t)INT32_MAX + 1, 0, &t) == -1);
|
||||
check("abs int32 下界接受", meow_time32_abs_from_parts(INT32_MIN, 0, &t) == 0);
|
||||
check("abs int32-1 拒绝", meow_time32_abs_from_parts((int64_t)INT32_MIN - 1, 0, &t) == -1);
|
||||
check("abs 2038 后一秒拒绝", meow_time32_abs_from_parts(2147483648LL, 0, &t) == -1);
|
||||
}
|
||||
|
||||
static void test_itimerspec(void)
|
||||
{
|
||||
struct meow_time64_itimerspec in;
|
||||
struct meow_itimerspec32 out, back32;
|
||||
struct meow_time64_itimerspec back;
|
||||
|
||||
in.it_interval.tv_sec = 1;
|
||||
in.it_interval.tv_nsec = 250000000;
|
||||
in.it_value.tv_sec = 3600;
|
||||
in.it_value.tv_nsec = 999999999;
|
||||
memset(&out, 0xAA, sizeof out);
|
||||
check("itimerspec 四字段转换成功",
|
||||
meow_time32_itimerspec_from_64(&in, &out) == 0);
|
||||
check_time32("itimerspec interval", &out.it_interval, 1, 250000000);
|
||||
check_time32("itimerspec value", &out.it_value, 3600, 999999999);
|
||||
|
||||
/* 越界必须整体失败且不改 *out(timerfd_settime 回 ENOTSUP 的前提) */
|
||||
memcpy(&back32, &out, sizeof back32);
|
||||
in.it_value.tv_sec = (int64_t)INT32_MAX + 1;
|
||||
check("itimerspec 越界拒绝", meow_time32_itimerspec_from_64(&in, &out) == -1);
|
||||
check("itimerspec 越界不写坏输出", memcmp(&back32, &out, sizeof out) == 0);
|
||||
|
||||
/* 反向(timerfd_gettime)字段不串位 */
|
||||
memset(&back, 0, sizeof back);
|
||||
meow_time64_itimerspec_from_32(&back32, &back);
|
||||
check("itimerspec 反向 sec/nsec 不串位",
|
||||
back.it_interval.tv_sec == 1 && back.it_interval.tv_nsec == 250000000 &&
|
||||
back.it_value.tv_sec == 3600 && back.it_value.tv_nsec == 999999999);
|
||||
}
|
||||
|
||||
/* 结构宽度证明:同一块内存按「libc 头 ABI」与「内核 o32 ABI」读出的值不同。 */
|
||||
static void test_struct_width(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
if (sizeof(time_t) != 8 || offsetof(struct timespec, tv_nsec) != 8) {
|
||||
printf("skip 结构宽度模拟(宿主 time_t/timespec 布局与 ILP32+time64 不一致)\n");
|
||||
return;
|
||||
}
|
||||
|
||||
/* 剩 0.4s:16 字节 time64 结构的前 8 字节被内核读成 {sec=0, nsec=0} → 立即超时 */
|
||||
memset(&ts, 0, sizeof ts);
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = 400000000;
|
||||
struct meow_time32 old_view;
|
||||
kernel_time32_view(&ts, &old_view);
|
||||
check("旧 ABI:0.4s 被内核读成 {0,0}(立即超时)",
|
||||
old_view.tv_sec == 0 && old_view.tv_nsec == 0);
|
||||
|
||||
/* 剩 2.3s:旧 ABI 丢掉 0.3s,只剩 2s 整 */
|
||||
memset(&ts, 0, sizeof ts);
|
||||
ts.tv_sec = 2;
|
||||
ts.tv_nsec = 300000000;
|
||||
kernel_time32_view(&ts, &old_view);
|
||||
check("旧 ABI:2.3s 被内核读成 2s 整",
|
||||
old_view.tv_sec == 2 && old_view.tv_nsec == 0);
|
||||
|
||||
/* 新路径:先转换再交给内核,宽度与值都对 */
|
||||
struct meow_time32 fixed;
|
||||
meow_time32_rel_from_parts((int64_t)ts.tv_sec, (int64_t)ts.tv_nsec, &fixed);
|
||||
struct meow_time32 new_view;
|
||||
kernel_time32_view(&fixed, &new_view);
|
||||
check("新 ABI:内核读到 {2, 300000000}", new_view.tv_sec == 2 &&
|
||||
new_view.tv_nsec == 300000000);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_rel();
|
||||
test_abs();
|
||||
test_itimerspec();
|
||||
test_struct_width();
|
||||
|
||||
printf(g_fail ? "== FAIL ==\n" : "== PASS ==\n");
|
||||
return g_fail;
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 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"
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 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"
|
||||
|
||||
@@ -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>,验证持久目录读写
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 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"
|
||||
|
||||
@@ -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_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,248 @@
|
||||
/* test_meow_ui_listview_rowh.c — 宿主回归(M15):listview 默认行高必须由字体行高派生,
|
||||
* 首行(scroll=0)标题必须真正画出来。
|
||||
*
|
||||
* 根因(M12 finding):双行 renderer 把「标题+副标题」文本块
|
||||
* block = 2 * meow_ui_text_height(font_body) + MEOW_LISTV_SUB_GAP(2)
|
||||
* 在行内垂直居中(ty = 行顶 + (item - block)/2),且文本只在 `ty >= 视口顶` 时才画。
|
||||
* 真机 source_han_serif 20px(ascent 24 + descent 6)行高 = 30 → block = 62;
|
||||
* 旧 SDK 默认行高固定 56 < 62 → ty = 行顶 - 3,首行(行顶 == 视口顶)标题被整条跳过,
|
||||
* 只剩副标题 → 所有 App 的两行列表首行标题消失。
|
||||
*
|
||||
* 宿主 5x7 字体行高是 14,照原样永远复现不了。故用链接期 `-Wl,--wrap=meow_ui_text_height`
|
||||
* 把行高替换成真机实测的 30,驱动同一份 SDK 代码(同 M12 app/test/test_first_row_title_render.c):
|
||||
* A 行高 30 + 显式 item_h=56 → 首行标题带 0 像素(bug 形态,防回归哨兵)
|
||||
* B 行高 30 + 不设 item_h → 派生 62 = block,首行标题/副标题都有像素
|
||||
* C 行高 30 + 显式 item_h=70 → ncm 语义(App 自管卡高)原样生效,标题可见
|
||||
* 断言全部读 back_buf 真实像素(region_has),非目检。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -o test_meow_ui_listview_rowh test_meow_ui_listview_rowh.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* meow_jpeg.c meow_png_stream.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz \
|
||||
* -Wl,--wrap=meow_ui_text_height
|
||||
* 运行:./test_meow_ui_listview_rowh
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* --wrap 替换实现:真机 source_han_serif 20px 实测行高(font.idx ascent+descent)。 */
|
||||
int __wrap_meow_ui_text_height(int size)
|
||||
{
|
||||
(void)size;
|
||||
return 30;
|
||||
}
|
||||
|
||||
#define LH 30 /* 真机正文行高 */
|
||||
#define BLOCK (2 * LH + 2) /* 双行文本块 = 62(与 MEOW_LISTV_SUB_GAP 同口径) */
|
||||
#define LV_X 24
|
||||
#define LV_Y 40
|
||||
#define LV_W 432
|
||||
#define LV_H 180
|
||||
#define NR 5 /* 行数 */
|
||||
|
||||
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)
|
||||
|
||||
/* ---- 数据源:src2(每帧取行)与 pool(count/bind)各一套,行内容相同 ---- */
|
||||
static const char *const k_title[NR] = { "T1", "T2", "T3", "T4", "T5" };
|
||||
static const char *const k_sub[NR] = { "S1", "S2", "S3", "S4", "S5" };
|
||||
|
||||
static int src2_cb(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= NR) return 0;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = k_title[idx];
|
||||
out->subtitle = k_sub[idx];
|
||||
out->right = MEOW_LIST_RIGHT_NONE;
|
||||
out->enabled = 1;
|
||||
return 1;
|
||||
}
|
||||
static int pool_count(void *ctx) { (void)ctx; return NR; }
|
||||
static void pool_bind(int idx, meow_lv_item *it, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
memset(it, 0, sizeof *it);
|
||||
if (idx < 0 || idx >= NR) return;
|
||||
it->title = k_title[idx];
|
||||
it->subtitle = k_sub[idx];
|
||||
it->enabled = 1;
|
||||
}
|
||||
|
||||
/* 造一个 src2 双行列表(扁平基线,便于按颜色断言行内文本带)。不设 item_h → 走派生。 */
|
||||
static meow_widget *make_lv(void)
|
||||
{
|
||||
meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, NR,
|
||||
src2_cb, NULL, MEOW_LIST_SEL_NONE);
|
||||
if (!lv) return NULL;
|
||||
meow_listview_set_container(lv, 0);
|
||||
meow_listview_set_item_renderer(lv, 2, 0, 0); /* 双行、无图标、无尾件 → text_x = LV_X+16 */
|
||||
return lv;
|
||||
}
|
||||
|
||||
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 os_r, os_g, os_b, osv_r, osv_g, osv_b;
|
||||
UNPK(th->onSurface, os_r, os_g, os_b);
|
||||
UNPK(th->onSurfaceVariant, osv_r, osv_g, osv_b);
|
||||
const int text_x = LV_X + 16; /* show_icon=0, container=0 */
|
||||
|
||||
/* ---- T0 --wrap 生效:宿主行高被替换为真机 30 ---- */
|
||||
CHECK(meow_ui_text_height(th->font_body) == LH, "wrap line_h=%d (expect %d)",
|
||||
meow_ui_text_height(th->font_body), LH);
|
||||
|
||||
/* ---- T1 默认(不设 item_h):行高按字体行高派生,首行标题可见 ---- */
|
||||
{
|
||||
meow_widget *lv = make_lv();
|
||||
CHECK(lv != NULL, "create");
|
||||
CHECK(meow_listview_item_h(lv) == BLOCK, "derived item_h=%d (expect %d = 2*%d+2)",
|
||||
meow_listview_item_h(lv), BLOCK, LH);
|
||||
CHECK(meow_listview_item_h(lv) >= 2 * LH + 2, "item_h >= 2*line_h + gap");
|
||||
CHECK(meow_listview_scroll(lv) == 0, "scroll=0");
|
||||
CHECK(meow_listview_max_scroll(lv) == NR * BLOCK - LV_H, "max_scroll=%d",
|
||||
meow_listview_max_scroll(lv));
|
||||
CHECK(meow_listview_hit(lv, LV_X + 10, LV_Y) == 0, "hit row0");
|
||||
CHECK(meow_listview_hit(lv, LV_X + 10, LV_Y + BLOCK) == 1, "hit row1 (item_h=%d)",
|
||||
meow_listview_item_h(lv));
|
||||
|
||||
int ty = LV_Y + (BLOCK - BLOCK) / 2; /* 62-62 → 首行标题顶 == 视口顶 */
|
||||
int sy = ty + LH + 2;
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(lv);
|
||||
CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2,
|
||||
"首行标题可见(scroll=0, ty=%d)", ty);
|
||||
CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2,
|
||||
"首行副标题可见(sy=%d)", sy);
|
||||
CHECK(region_has(text_x, LV_Y - 20, 60, 20, os_r, os_g, os_b) == 0,
|
||||
"视口上方无标题泄漏");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ---- T2 裸 create(不调 renderer):默认 lines=2 同样派生 ---- */
|
||||
{
|
||||
meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, NR,
|
||||
src2_cb, NULL, MEOW_LIST_SEL_NONE);
|
||||
CHECK(lv != NULL && meow_listview_item_h(lv) == BLOCK, "bare create item_h=%d",
|
||||
lv ? meow_listview_item_h(lv) : -1);
|
||||
/* set_item_h(<=0) 忽略:仍返回派生值 */
|
||||
CHECK(meow_listview_set_item_h(lv, 0) == BLOCK, "set_item_h(0) keeps derived");
|
||||
CHECK(meow_listview_item_h(lv) == BLOCK, "item_h unchanged after set_item_h(0)");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ---- T3 显式 set_item_h 优先(ncm 语义:App 按自身卡高传 70)---- */
|
||||
{
|
||||
meow_widget *lv = make_lv();
|
||||
CHECK(meow_listview_set_item_h(lv, 70) == 70, "explicit item_h returns 70");
|
||||
CHECK(meow_listview_item_h(lv) == 70, "explicit item_h=70 wins over derived %d", BLOCK);
|
||||
CHECK(meow_listview_max_scroll(lv) == NR * 70 - LV_H, "max_scroll uses 70");
|
||||
int ty = LV_Y + (70 - BLOCK) / 2; /* 4 */
|
||||
int sy = ty + LH + 2;
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(lv);
|
||||
CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "显式 70 首行标题可见");
|
||||
CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "显式 70 首行副标题可见");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ---- T4 档位:tier 恢复派生默认(2→62 / 1→48),单行不画副标题 ---- */
|
||||
{
|
||||
meow_widget *lv = make_lv();
|
||||
CHECK(meow_listview_set_item_h(lv, 70) == 70, "pre-set explicit 70");
|
||||
CHECK(meow_listview_set_item_tier(lv, 2) == 0, "tier(2)");
|
||||
CHECK(meow_listview_item_h(lv) == BLOCK, "tier(2) 派生 item_h=%d", meow_listview_item_h(lv));
|
||||
CHECK(meow_listview_set_item_tier(lv, 1) == 0, "tier(1)");
|
||||
CHECK(meow_listview_item_h(lv) == 48, "tier(1) item_h=%d (expect 48)",
|
||||
meow_listview_item_h(lv));
|
||||
int ty = LV_Y + (48 - LH) / 2; /* 9 */
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(lv);
|
||||
CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "单行档位标题可见");
|
||||
CHECK(region_has(text_x, LV_Y + 41, 60, 6, osv_r, osv_g, osv_b) == 0,
|
||||
"单行档位不画副标题");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ---- T5 pool 模式(set_vsource + 默认 renderer):派生行高 + 首行标题可见 ---- */
|
||||
{
|
||||
meow_widget *lv = meow_listview_create(LV_X, LV_Y, LV_W, LV_H, 0,
|
||||
NULL, NULL, MEOW_LIST_SEL_NONE);
|
||||
meow_listview_set_container(lv, 0);
|
||||
CHECK(meow_listview_set_vsource(lv, pool_count, pool_bind, NULL) == 0, "vsource");
|
||||
CHECK(meow_listview_set_item_renderer(lv, 2, 0, 0) == 0, "renderer(2,0,0)");
|
||||
CHECK(meow_listview_item_h(lv) == BLOCK, "pool item_h=%d", meow_listview_item_h(lv));
|
||||
int ty = LV_Y + (BLOCK - BLOCK) / 2;
|
||||
int sy = ty + LH + 2;
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(lv);
|
||||
CHECK(region_has(text_x, ty, 60, LH, os_r, os_g, os_b) > 2, "pool 首行标题可见");
|
||||
CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2, "pool 首行副标题可见");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ---- T6 旧行为哨兵:显式 56(< block 62)→ 首行标题仍被裁剪守卫跳过 ----
|
||||
* 记录 bug 形态,证明上面的像素断言确实能区分「画出」与「跳过」;
|
||||
* 派生默认生效后这条路径只剩「App 明确要 56」时才会走到。 */
|
||||
{
|
||||
meow_widget *lv = make_lv();
|
||||
CHECK(meow_listview_set_item_h(lv, 56) == 56, "explicit 56");
|
||||
CHECK(meow_listview_item_h(lv) == 56, "explicit 56 stays 56");
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget_draw(lv);
|
||||
int ty = LV_Y + (56 - BLOCK) / 2; /* -3 → 行顶上方 */
|
||||
int sy = ty + LH + 2; /* 29 */
|
||||
CHECK(region_has(text_x, LV_Y, 60, LH, os_r, os_g, os_b) == 0,
|
||||
"旧 56 行高:首行标题被跳过(bug 形态)");
|
||||
CHECK(region_has(text_x, sy, 60, LH, osv_r, osv_g, osv_b) > 2,
|
||||
"旧 56 行高:首行副标题仍画(症状「只有副标题」)");
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -10,11 +10,11 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 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"
|
||||
@@ -206,6 +206,7 @@ int main(void)
|
||||
/* 双行(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");
|
||||
@@ -267,6 +268,7 @@ int main(void)
|
||||
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);
|
||||
|
||||
+227
-3
@@ -4,11 +4,12 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* meow_jpeg.c meow_png_stream.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
|
||||
* 运行:./test_meow_ui_scroll
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
@@ -169,15 +170,238 @@ static void test_stop(void)
|
||||
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,181 @@
|
||||
/* 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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* meow_jpeg.c meow_png_stream.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
|
||||
* 运行:./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。
|
||||
* meow_listview 默认容器模式(行=surfaceContainer 12dp 圆角卡 + MEOW_LISTV_CARD_MARGIN=8 横向内缩),
|
||||
* 选中卡左缘=8、文本左缘=24;采样点取 x=10(卡内、文本左侧),避免取到左侧空白边距(旧 x=6 落在卡外)。 */
|
||||
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 = 10, hy = list_y + 2 * SEL_ROW + SEL_ROW / 2;
|
||||
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;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* 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.
|
||||
* 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,仅验证内嵌双语表(无覆盖表)
|
||||
|
||||
@@ -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -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;
|
||||
}
|
||||
@@ -8,10 +8,11 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_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_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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -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_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -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"
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_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_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。
|
||||
@@ -318,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;
|
||||
}
|
||||
@@ -15,9 +15,10 @@
|
||||
*
|
||||
* 编译(宿主 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_str.c meow_ui_media.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* 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,14 +15,15 @@
|
||||
* 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 \
|
||||
* test_host_stub.c meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_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 \
|
||||
@@ -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
@@ -8,10 +8,10 @@
|
||||
*
|
||||
* 编译(宿主 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_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 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"
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* 3. 图标簇+值:fed 数据 → render 依次调用 meow_gfx_blit_icon(电量/音量/蓝牙/WiFi/亮度)
|
||||
* 与 meow_ui_text_right(%n 值);WiFi 已连-50dBm → WIFI_STRONG。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_sb_statusbar test_sb_statusbar.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c \
|
||||
* -I. -I../meow_launcherd
|
||||
* 编译(宿主 gcc;测试自带桩与 meow_ui_gfx/meow_ui_log 的符号重复,需 --allow-multiple-definition):
|
||||
* gcc -Wall -Wextra -o test_sb_statusbar test_sb_statusbar.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. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -Wl,--allow-multiple-definition
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -Wall -o test_sdk_fixes test_sdk_fixes.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* 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"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
* 编译(宿主 gcc,仅链 vol/media 两个 TU;无需 meow_ui 其它子系统):
|
||||
* gcc -Wall -Wextra -pthread -o test_sdk_input_keys test_sdk_input_keys.c \
|
||||
* meow_ui_vol.c meow_ui_media.c -I. -I../meow_launcherd
|
||||
* meow_ui_vol.c meow_ui_media.c -I. -Iinclude
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* - on_preempt 回调:第二客户端(raw)抢先 → 本 SDK 客户端收到 PREEMPTED,write 返回负值
|
||||
*
|
||||
* 启动被测试 audiod(宿主二进制,FILE sink)。
|
||||
* 编译:cc -I../meow_launcherd -I. -Os -Wall -Wextra test_ui_audio_host.c meow_ui_audio.c -lpthread -o test_ui_audio_host
|
||||
* **需主仓组件,公开仓跳过**:运行需 ../meow_audiod/meow_audiod_host(未随本仓发布)。
|
||||
* 编译:cc -I. -Iinclude -Os -Wall -Wextra test_ui_audio_host.c meow_ui_audio.c -lpthread -lm -o test_ui_audio_host
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* test_umbrella.c — 伞头完整性回归:只 #include "meow_ui.h",抽查各模块代表性声明
|
||||
* (类型 / 函数 / 宏)都存在。头拆分后用来证明伞头聚合无遗漏、签名未变。
|
||||
* 用法:gcc -std=c11 -Wall -Wextra -I<sdk> -c test_umbrella.c -o test_umbrella.o
|
||||
* 用法(在 sdk/ 目录下):gcc -std=c11 -Wall -Wextra -I. -Iinclude -c test_umbrella.c -o test_umbrella.o
|
||||
* (纯声明级检查,不链接、不调用;产生可执行代码仍 0 warning)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# tuku(os.meow.picbrowser)打包与签名
|
||||
|
||||
图库 App(系统包名 `os.meow.picbrowser`)。构建与打包:
|
||||
|
||||
```sh
|
||||
./build_tuku.sh # 交叉编译 picbrowser(MIPS32r2 静态)
|
||||
./pack_tuku.sh # 打包 → os.meow.picbrowser.mpk(根布局;未签名)
|
||||
```
|
||||
|
||||
## 浏览交互(文件管理器模型,M18)
|
||||
|
||||
- 列表 = 当前目录一层的**子目录 + 受支持图片(png/jpg/jpeg/bmp)+ 档案册(.cbz/.zip)**;
|
||||
目录优先,组内自然序(大小写不敏感,同键按原始字节兜底,`A.jpg`/`a.jpg` 不抖动)。
|
||||
- 点目录 = 进入(空目录也进);点图片 = 从该图开始读(按**文件名**交给阅读器,
|
||||
由阅读器的全量目录册定位起始页——列表被 `MR_LIB_SCAN_MAX` 截断时也不会点错图);
|
||||
点档案册 = 打开档案册(第 1 页/续读)。
|
||||
- 单目录超过 2000 项时列表只显示前 2000 项并给出「已截断」提示行;图片仍可进入阅读器翻到。
|
||||
- 缩略图功能已关闭:行内只有类型图标 + 文件名 + 类型·大小;首帧只做单层
|
||||
`opendir/readdir + lstat`,滚动/点击路径不解码、不访问文件、不重排字符串。
|
||||
- 浏览根锁定 TF 卡根(`/usr/data/mnt/sd_0`):根目录不显示返回箭头,左缘返回 = 退出 App;
|
||||
**不允许上溯到 SD 卡的上级目录**。
|
||||
- 根目录不可访问(未插卡/挂载失败)→ 友好空态「未检测到 SD 卡」;点顶栏刷新或重进页面即重扫,
|
||||
不弹错、不崩溃。
|
||||
|
||||
## 一键签名出包
|
||||
|
||||
```sh
|
||||
MEOW_SIGN_KEY=/path/to/signing.key ./pack_tuku.sh
|
||||
```
|
||||
|
||||
- 工具查找顺序:`$MEOW_SIGN` → `PATH` 里的 `meow_sign` → `$HOME/Proj/MeowOS-For-R3P2/patch_v2/meow_sign/meow_sign`。
|
||||
- 私钥:`$MEOW_SIGN_KEY`。未设置时脚本**警告并仍产出未签名包**(退出码 0);但 `os.meow.*` 系统包无签名会被设备拒装。
|
||||
- 有工具 + 密钥时执行 `meow_sign sign <mpk> <key>`;若存在 `<key>.pub` 再执行 `meow_sign verify <mpk> <key>.pub`。
|
||||
- 私钥**不入库、不随包分发**;本仓不包含任何密钥材料。
|
||||
|
||||
## mpk 布局
|
||||
|
||||
`.mpk` = tar.gz,条目直接在根层:
|
||||
|
||||
```
|
||||
os.meow.picbrowser.mpk
|
||||
├── app.json
|
||||
├── picbrowser
|
||||
└── <signature footer> # 仅签名包有(Ed25519,尾部追加)
|
||||
```
|
||||
|
||||
即 `tar -C build/os.meow.picbrowser -czf os.meow.picbrowser.mpk app.json picbrowser`——
|
||||
不带 `os.meow.picbrowser/` 目录前缀,也不带 `./` 前缀。脚本在签名前用 `tar -tzf` 断言这一点。
|
||||
|
||||
签名是往 tar.gz 尾部**追加** footer,签名后的 mpk 不要再跑 `tar -t` / `gzip` 之类的命令:
|
||||
gzip 会报 `trailing garbage ignored`,属预期现象,不是包损坏。要检查布局请在签名前做。
|
||||
|
||||
## 安装
|
||||
|
||||
把 `.mpk` 放到设备 TF 卡,执行 `nyai install os.meow.picbrowser.mpk`;
|
||||
`os.meow.*` 包必须验签通过(footer.package == 文件名茎 == `app.json.package`)。
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/* app.c — 图库装配层:main + 建页 + meow_ui_run 回调路由 + 主题监听。
|
||||
*
|
||||
* 契约(docs/APP_DEV_GUIDE.md §2-§3):
|
||||
* - main 只做 meow_ui_init → 建 view → meow_ui_run → 清理;退出走 meow_ui_request_exit;
|
||||
* - 页面切换统一走 meow_view_page_push/pop(activity 栈 + on_enter/on_leave 唯一入口);
|
||||
* - 物理键不映射返回/确认(on_key 仅日志);屏底上滑由 SDK 系统手势接管,App 不拦截;
|
||||
* - 左缘返回:reader/settings=pop;lock 页自行消费;书库逐级上移,
|
||||
* **到 TF 卡根=退出 App**(浏览根锁定,不再上溯系统目录;顶栏也没有越根的「上级」)。
|
||||
*
|
||||
* 主题同步:注册 meow_theme_add_listener;明暗/风格族切换时更新页面底色并整帧 invalidate,
|
||||
* 各页 on_draw / SDK 控件下一帧即用新语义色重绘(不缓存颜色)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "ui/reader/reader.h"
|
||||
#include "meow_ui_nav.h"
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
static void on_sigterm(int s)
|
||||
{
|
||||
(void)s;
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
|
||||
/* 向 launcher 上报当前页栈深/顶层,供系统导航栈跟随。各页 on_enter 调用。 */
|
||||
void mr_nav_report(void)
|
||||
{
|
||||
int depth = meow_ui_activity_depth();
|
||||
if (depth <= 0) depth = 1;
|
||||
int id = meow_ui_activity_active();
|
||||
const char *top = (id == ACT_READER) ? "reader"
|
||||
: (id == ACT_SETTINGS) ? "settings"
|
||||
: (id == ACT_LOCK) ? "lock"
|
||||
: "library";
|
||||
meow_ui_nav_report((uint32_t)depth, top);
|
||||
}
|
||||
|
||||
/* 主题切换:页面底色 + 整帧重绘(控件与 on_draw 全部从 meow_theme_get() 取色)。 */
|
||||
static void app_theme_changed(meow_theme_mode m)
|
||||
{
|
||||
(void)m;
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||
}
|
||||
|
||||
static int app_active(void)
|
||||
{
|
||||
return meow_ui_activity_active();
|
||||
}
|
||||
|
||||
static void app_on_key(int keycode, int is_down)
|
||||
{
|
||||
(void)keycode; (void)is_down; /* 物理键是系统语义,不做返回/确认映射 */
|
||||
meow_log(MEOW_LOG_DEBUG, "key");
|
||||
}
|
||||
|
||||
static void app_on_tap(int x, int y)
|
||||
{
|
||||
switch (app_active()) {
|
||||
case ACT_LIBRARY: library_on_tap(x, y); break;
|
||||
case ACT_READER: reader_on_tap(x, y); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static void app_on_swipe(int dir, int dx, int dy)
|
||||
{
|
||||
(void)dy;
|
||||
if (dir == MEOW_SWIPE_LEFT) {
|
||||
if (app_active() == ACT_LIBRARY) {
|
||||
if (!library_go_up()) {
|
||||
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
} else {
|
||||
meow_view_page_pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* SDK 的 on_swipe 把位移量放在第二参(第三参为 0):UP/DOWN 时 dx=纵向位移。 */
|
||||
if (app_active() == ACT_LIBRARY) library_on_swipe(dir, dx);
|
||||
}
|
||||
|
||||
/* 通用长按(SDK §6.3)是 opt-in:未注册回调时 meow_input_long_press_poll 直接返回 0,
|
||||
* MEOW_EV_LONG_PRESS 永远不会产生 —— 锁屏的「长按退格清空」在真机上就退化成单删一位。
|
||||
* 这里注册一个 no-op 回调把长按打开;事件仍然先经 meow_view_dispatch_event 路由,锁屏页
|
||||
* 自己消费 LONG_PRESS(其它页命中不到就走 no-op)。
|
||||
*
|
||||
* 只在锁屏为顶层页时启用:长按一旦触发,其抬手不再产生 TAP/SWIPE(SDK 抑制点击),
|
||||
* 全程开启会让书库/阅读器里「按住稍久」的点击被吞掉。锁屏页解锁 pop 后由 app_on_tick
|
||||
* 下一拍关掉,避免把长按语义泄漏到其它页。 */
|
||||
static int g_long_press_on;
|
||||
|
||||
static void app_on_long_press(int x, int y)
|
||||
{
|
||||
(void)x; (void)y; /* 锁屏已在 view 分发里消费;其余页没有长按语义 */
|
||||
}
|
||||
|
||||
static void app_sync_long_press(void)
|
||||
{
|
||||
int on = (app_active() == ACT_LOCK);
|
||||
if (on == g_long_press_on) return;
|
||||
meow_ui_set_long_press_cb(on ? app_on_long_press : NULL);
|
||||
g_long_press_on = on;
|
||||
}
|
||||
|
||||
static void app_on_tick(void)
|
||||
{
|
||||
app_sync_long_press();
|
||||
library_on_tick();
|
||||
}
|
||||
|
||||
static void app_on_swipe_progress(int x_start, int dx)
|
||||
{
|
||||
meow_ui_swipe_hint(x_start + dx);
|
||||
}
|
||||
|
||||
/* 跟手拖动路由(M6):ACT_READER 需要原始 down/move 算位移;其余页保持原语义。
|
||||
* 屏底热区/左缘的避让由 reader 侧按同一口径判断(reader_on_touch_down)。 */
|
||||
static void app_on_touch_down(int x, int y)
|
||||
{
|
||||
if (app_active() == ACT_LIBRARY) library_on_touch_down(x, y);
|
||||
else if (app_active() == ACT_READER) reader_on_touch_down(x, y);
|
||||
}
|
||||
|
||||
static void app_on_touch_move(int x, int y)
|
||||
{
|
||||
if (app_active() == ACT_LIBRARY) library_on_touch_move(x, y);
|
||||
else if (app_active() == ACT_READER) reader_on_touch_move(x, y);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_log(MEOW_LOG_INFO, "start");
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "init failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
meow_theme_add_listener(app_theme_changed);
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
meow_ui_nav_init();
|
||||
signal(SIGTERM, on_sigterm);
|
||||
|
||||
/* 先建全部页面(on_create 建各自控件/扫描书库),再按启动锁状态进根页。 */
|
||||
meow_view *library = library_view_create();
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view *settings = settings_view_create();
|
||||
meow_view *lock = lock_view_create();
|
||||
|
||||
meow_view_page_push(lock_view_should_show() ? ACT_LOCK : ACT_LIBRARY);
|
||||
app_sync_long_press(); /* 进主循环前先对齐一次:启动即锁屏时首帧长按就可用 */
|
||||
|
||||
meow_ui_events ev = {
|
||||
app_on_key, app_on_tap, app_on_swipe, app_on_tick,
|
||||
app_on_swipe_progress, app_on_touch_down, app_on_touch_move,
|
||||
};
|
||||
meow_ui_run(&ev);
|
||||
|
||||
meow_view_free(lock);
|
||||
meow_view_free(settings);
|
||||
meow_view_free(reader);
|
||||
meow_view_free(library);
|
||||
meow_ui_shutdown();
|
||||
meow_log(MEOW_LOG_INFO, "exit");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"name":"图库","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.picbrowser/picbrowser",
|
||||
"package":"os.meow.picbrowser","version":"0.1","version_code":1,"framework":1,
|
||||
"fonts":"common","route.reader.fullscreen":1}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# build_tuku.sh — 构建图库(静态 MIPS32r2,链 meow_ui.a + libz.a + -lpthread)。
|
||||
#
|
||||
# SDK 路径用绝对路径(默认 $HOME/Proj/MeowOS-SDK/sdk),可用 MEOW_SDK_DIR 覆盖;
|
||||
# 故意不用 ../../sdk —— 交付时 tuku/ 会搬到独立仓 ~/Proj/Meow-OS-APPs/os.meow.picbrowser。
|
||||
# 交叉编译器/sysroot 同 SDK:MEOW_CC / MEOW_SYSROOT 可覆盖。
|
||||
# -I"$SYSROOT/include":zip 容器层(logic/zip/zip.c)直接 include <zlib.h>,
|
||||
# 头文件随 sysroot 的 libz.a 一起提供(SDK 自身只用最小 zlib ABI 自声明)。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
CC="${MEOW_CC:-$HOME/.tools/zig-mipsel-cc}"
|
||||
SYSROOT="${MEOW_SYSROOT:-$HOME/.tools/mipsel-linux-musleabi-sysroot}"
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
|
||||
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I"$SYSROOT/include" -I. \
|
||||
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c logic/pin.c \
|
||||
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
|
||||
ls -la picbrowser
|
||||
file picbrowser
|
||||
@@ -0,0 +1,462 @@
|
||||
/* logic/library.c — 书库文件管理器模型:单层枚举 + 纯内存行合成(纯 C)。
|
||||
*
|
||||
* 扫描(mr_library_scan)与 os.meow.files 的 fsx_scan_dir 同构:
|
||||
* - 一次 opendir/readdir,逐项 lstat(只认目录、受支持图片、.cbz/.zip;符号链接跳过防环);
|
||||
* - 隐藏项("." 开头,含 . ..)跳过;空目录/无图目录/系统目录照常列出;
|
||||
* - 排序:目录优先,组内按 mr_natural_cmp 自然升序;
|
||||
* - 副标题(类型 · 大小)在扫描期生成一次,渲染期不再做任何格式化/IO;
|
||||
* - 条目到 MR_LIB_SCAN_MAX 后仍有条目 → 截断标注。
|
||||
*
|
||||
* 阅读器辅助(与列表模型解耦):mr_library_dir_has_images 单层判据 +
|
||||
* mr_book_adjacent(_ex) 兄弟册衔接(M7/M11 阅读器翻到相邻册用)。
|
||||
*/
|
||||
#include "library.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef MR_LIBRARY_TEST_HOOKS
|
||||
static mr_library_io_stats g_io;
|
||||
|
||||
void mr_library_test_io_reset(void)
|
||||
{
|
||||
memset(&g_io, 0, sizeof g_io);
|
||||
}
|
||||
|
||||
void mr_library_test_io_get(mr_library_io_stats *out)
|
||||
{
|
||||
if (out) *out = g_io;
|
||||
}
|
||||
|
||||
#define IO_OPEN() (g_io.opendirs++)
|
||||
#define IO_READDIR() (g_io.readdirs++)
|
||||
#define IO_LSTAT() (g_io.lstats++)
|
||||
#else
|
||||
#define IO_OPEN() ((void)0)
|
||||
#define IO_READDIR() ((void)0)
|
||||
#define IO_LSTAT() ((void)0)
|
||||
#endif
|
||||
|
||||
/* 系统目录不参与**册间衔接**(M7 兄弟册表);浏览本身已放开(不隐藏)。 */
|
||||
static int ignored_dir_name(const char *name)
|
||||
{
|
||||
static const char *const skip[] = { "MeowAPPs", "MeowData", "MeowOS", "lost+found" };
|
||||
if (!name || name[0] == '.') return 1;
|
||||
for (unsigned i = 0; i < sizeof skip / sizeof skip[0]; i++)
|
||||
if (strcmp(name, skip[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int copy_str(char *dst, int dstsz, const char *src)
|
||||
{
|
||||
if (!dst || dstsz <= 0) return -1;
|
||||
if (!src) { dst[0] = '\0'; return 0; }
|
||||
int n = (int)strlen(src);
|
||||
if (n >= dstsz) return -1;
|
||||
memcpy(dst, src, (size_t)n + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int join_path(char *out, int outsz, const char *dir, const char *name)
|
||||
{
|
||||
if (!out || outsz <= 0 || !dir || !name) return -1;
|
||||
int n;
|
||||
size_t dl = strlen(dir);
|
||||
if (dl > 0 && dir[dl - 1] == '/')
|
||||
n = snprintf(out, (size_t)outsz, "%s%s", dir, name);
|
||||
else
|
||||
n = snprintf(out, (size_t)outsz, "%s/%s", dir, name);
|
||||
return (n > 0 && n < outsz) ? 0 : -1;
|
||||
}
|
||||
|
||||
int mr_path_is_image(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "png") == 0 || strcasecmp(dot, "jpg") == 0 ||
|
||||
strcasecmp(dot, "jpeg") == 0 || strcasecmp(dot, "bmp") == 0;
|
||||
}
|
||||
|
||||
int mr_path_is_archive(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "cbz") == 0 || strcasecmp(dot, "zip") == 0;
|
||||
}
|
||||
|
||||
int mr_natural_cmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
const unsigned char *pb = (const unsigned char *)b;
|
||||
if (!a || !b) return (a == b) ? 0 : (a ? 1 : -1);
|
||||
|
||||
while (*pa && *pb) {
|
||||
if (isdigit(*pa) && isdigit(*pb)) {
|
||||
const unsigned char *za = pa, *zb = pb;
|
||||
while (*pa == '0') pa++;
|
||||
while (*pb == '0') pb++;
|
||||
const unsigned char *da = pa, *db = pb;
|
||||
while (isdigit(*pa)) pa++;
|
||||
while (isdigit(*pb)) pb++;
|
||||
|
||||
size_t la = (size_t)(pa - da), lb = (size_t)(pb - db);
|
||||
if (la != lb) return la < lb ? -1 : 1;
|
||||
if (la > 0) {
|
||||
int c = memcmp(da, db, la);
|
||||
if (c != 0) return c < 0 ? -1 : 1;
|
||||
}
|
||||
size_t oa = (size_t)(da - za), ob = (size_t)(db - zb);
|
||||
if (oa != ob) return oa < ob ? -1 : 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int ca = tolower(*pa), cb = tolower(*pb);
|
||||
if (ca != cb) return ca < cb ? -1 : 1;
|
||||
pa++;
|
||||
pb++;
|
||||
}
|
||||
if (*pa) return 1;
|
||||
if (*pb) return -1;
|
||||
return strcmp(a, b) < 0 ? -1 : (strcmp(a, b) > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 扫描 / 排序 / 行合成 */
|
||||
/* ============================================================ */
|
||||
static void fmt_size(long long size, char *out, int outsz)
|
||||
{
|
||||
if (size < 0) size = 0;
|
||||
static const char *const unit[] = { "B", "KB", "MB", "GB" };
|
||||
double v = (double)size;
|
||||
int i = 0;
|
||||
while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; }
|
||||
if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size);
|
||||
else snprintf(out, (size_t)outsz, "%.1f %s", v, unit[i]);
|
||||
}
|
||||
|
||||
/* 副标题只在扫描期生成一次(渲染期零格式化)。 */
|
||||
static void format_subtitle(mr_library_entry *e)
|
||||
{
|
||||
char sz[32];
|
||||
switch (e->kind) {
|
||||
case MR_LIB_KIND_DIR:
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "文件夹");
|
||||
break;
|
||||
case MR_LIB_KIND_ARCHIVE:
|
||||
fmt_size(e->size, sz, (int)sizeof sz);
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "档案册 · %s", sz);
|
||||
break;
|
||||
default:
|
||||
fmt_size(e->size, sz, (int)sizeof sz);
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "图片 · %s", sz);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void mr_library_free(mr_library *lib)
|
||||
{
|
||||
if (!lib) return;
|
||||
free(lib->items);
|
||||
lib->items = NULL;
|
||||
lib->count = 0;
|
||||
lib->cap = 0;
|
||||
lib->truncated = 0;
|
||||
}
|
||||
|
||||
static int lib_add(mr_library *lib, const mr_library_entry *e)
|
||||
{
|
||||
if (lib->count >= lib->cap) {
|
||||
int cap = lib->cap ? lib->cap * 2 : 16;
|
||||
mr_library_entry *ni = (mr_library_entry *)realloc(lib->items,
|
||||
(size_t)cap * sizeof *ni);
|
||||
if (!ni) return -1;
|
||||
lib->items = ni;
|
||||
lib->cap = cap;
|
||||
}
|
||||
lib->items[lib->count++] = *e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 目录优先;组内自然序(大小写不敏感 + 原始字节兜底,结果确定)。 */
|
||||
static int entry_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const mr_library_entry *a = (const mr_library_entry *)pa;
|
||||
const mr_library_entry *b = (const mr_library_entry *)pb;
|
||||
int ad = (a->kind == MR_LIB_KIND_DIR), bd = (b->kind == MR_LIB_KIND_DIR);
|
||||
if (ad != bd) return ad ? -1 : 1;
|
||||
return mr_natural_cmp(a->name, b->name);
|
||||
}
|
||||
|
||||
static int add_entry(mr_library *lib, const char *name, const char *full,
|
||||
int kind, const struct stat *st)
|
||||
{
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
|
||||
e.kind = kind;
|
||||
e.size = (kind == MR_LIB_KIND_DIR) ? 0 : (long long)st->st_size;
|
||||
format_subtitle(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
(void)max_depth; /* 兼容旧签名:扫描不递归 */
|
||||
if (!lib) return -1;
|
||||
mr_library_free(lib);
|
||||
if (!dir || !dir[0]) return -1;
|
||||
|
||||
IO_OPEN();
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
IO_READDIR();
|
||||
const char *name = de->d_name;
|
||||
if (name[0] == '.') continue; /* . .. 与隐藏项 */
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
|
||||
|
||||
IO_LSTAT();
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
|
||||
int kind;
|
||||
if (S_ISDIR(st.st_mode)) kind = MR_LIB_KIND_DIR;
|
||||
else if (!S_ISREG(st.st_mode)) continue;
|
||||
else if (mr_path_is_image(name)) kind = MR_LIB_KIND_IMAGE;
|
||||
else if (mr_path_is_archive(name)) kind = MR_LIB_KIND_ARCHIVE;
|
||||
else continue;
|
||||
|
||||
if (lib->count >= MR_LIB_SCAN_MAX) { lib->truncated = 1; break; }
|
||||
if (add_entry(lib, name, full, kind, &st) != 0) {
|
||||
closedir(d);
|
||||
mr_library_free(lib);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (lib->count > 1)
|
||||
qsort(lib->items, (size_t)lib->count, sizeof *lib->items, entry_cmp);
|
||||
return lib->count;
|
||||
}
|
||||
|
||||
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
|
||||
{
|
||||
static char trunc_sub[96];
|
||||
static int trunc_ready;
|
||||
|
||||
if (!lib || !out) return 0;
|
||||
if (idx == lib->count && lib->truncated) {
|
||||
if (!trunc_ready) {
|
||||
/* 截断只是列表显示上限:图片仍能从任一可见图片进入阅读器后翻到,
|
||||
* 文案不要让人误以为「没有更多图片」。 */
|
||||
snprintf(trunc_sub, sizeof trunc_sub,
|
||||
"仅显示前 %d 项(图片可在阅读器中翻到)", MR_LIB_SCAN_MAX);
|
||||
trunc_ready = 1;
|
||||
}
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = "已截断";
|
||||
out->subtitle = trunc_sub;
|
||||
out->disabled = 1;
|
||||
out->index = -1;
|
||||
return 1;
|
||||
}
|
||||
if (idx < 0 || idx >= lib->count) return 0;
|
||||
|
||||
const mr_library_entry *e = &lib->items[idx];
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = e->name;
|
||||
out->subtitle = e->subtitle;
|
||||
out->kind = e->kind;
|
||||
out->index = idx;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_path_within_root(const char *path, const char *root)
|
||||
{
|
||||
if (!path || !path[0] || !root || !root[0]) return 0;
|
||||
size_t rl = strlen(root);
|
||||
while (rl > 1 && root[rl - 1] == '/') rl--;
|
||||
if (rl == 1 && root[0] == '/') return path[0] == '/';
|
||||
if (strncmp(path, root, rl) != 0) return 0;
|
||||
char c = path[rl];
|
||||
return c == '\0' || c == '/';
|
||||
}
|
||||
|
||||
int mr_path_parent(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!path || !out || outsz <= 0) return -1;
|
||||
size_t n = strlen(path);
|
||||
while (n > 1 && path[n - 1] == '/') n--;
|
||||
if (n <= 1) {
|
||||
if (outsz < 2) return -1;
|
||||
out[0] = '/';
|
||||
out[1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
while (n > 0 && path[n - 1] != '/') n--;
|
||||
while (n > 1 && path[n - 1] == '/') n--;
|
||||
if (n >= (size_t)outsz) return -1;
|
||||
memcpy(out, path, n);
|
||||
out[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 册间衔接(M7 / M11) */
|
||||
/* ============================================================ */
|
||||
int mr_library_dir_has_images(const char *dir)
|
||||
{
|
||||
if (!dir || !dir[0]) return 0;
|
||||
IO_OPEN();
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return 0;
|
||||
|
||||
int found = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
IO_READDIR();
|
||||
const char *name = de->d_name;
|
||||
if (name[0] == '.' || !mr_path_is_image(name)) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
|
||||
IO_LSTAT();
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
closedir(d);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int name_ptr_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const char *a = *(const char *const *)pa;
|
||||
const char *b = *(const char *const *)pb;
|
||||
return mr_natural_cmp(a, b);
|
||||
}
|
||||
|
||||
/* 兄弟册判据(M11):目录=直接含图;普通文件=档案册且有图片页。
|
||||
* 两者同列,交给 name_ptr_cmp 自然序混排。 */
|
||||
static int sibling_is_book(const char *full, const struct stat *st,
|
||||
mr_lib_archive_fn probe, void *ud)
|
||||
{
|
||||
if (S_ISDIR(st->st_mode)) return mr_library_dir_has_images(full);
|
||||
if (S_ISREG(st->st_mode)) {
|
||||
const char *slash = strrchr(full, '/');
|
||||
const char *name = slash ? slash + 1 : full;
|
||||
if (!mr_path_is_archive(name)) return 0;
|
||||
if (!probe) return 0;
|
||||
int n = probe(full, ud);
|
||||
return n > 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0] || (delta != 1 && delta != -1)) return -1;
|
||||
|
||||
char norm[MR_PATH_MAX];
|
||||
size_t dl = strlen(dir);
|
||||
while (dl > 1 && dir[dl - 1] == '/') dl--;
|
||||
if (dl >= sizeof norm) return -1;
|
||||
memcpy(norm, dir, dl);
|
||||
norm[dl] = '\0';
|
||||
|
||||
const char *slash = strrchr(norm, '/');
|
||||
if (!slash || !slash[1]) return -1;
|
||||
const char *cur = slash + 1;
|
||||
|
||||
char parent[MR_PATH_MAX];
|
||||
if (mr_path_parent(norm, parent, (int)sizeof parent) != 0) return -1;
|
||||
|
||||
DIR *d = opendir(parent);
|
||||
if (!d) return 0;
|
||||
|
||||
char **names = NULL;
|
||||
int n = 0, cap = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const char *name = de->d_name;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (ignored_dir_name(name)) continue;
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, parent, name) != 0) continue;
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
if (!sibling_is_book(full, &st, archive_pages, ud)) continue;
|
||||
|
||||
if (n >= cap) {
|
||||
int nc = cap ? cap * 2 : 16;
|
||||
char **nn = (char **)realloc(names, (size_t)nc * sizeof *nn);
|
||||
if (!nn) break;
|
||||
names = nn;
|
||||
cap = nc;
|
||||
}
|
||||
size_t len = strlen(name);
|
||||
names[n] = (char *)malloc(len + 1);
|
||||
if (!names[n]) break;
|
||||
memcpy(names[n], name, len + 1);
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
int rc = 0;
|
||||
if (n > 1) qsort(names, (size_t)n, sizeof *names, name_ptr_cmp);
|
||||
int idx = -1;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(names[i], cur) == 0) { idx = i; break; }
|
||||
int target = idx + delta;
|
||||
if (idx >= 0 && target >= 0 && target < n) {
|
||||
if (join_path(out, outsz, parent, names[target]) == 0) rc = 1;
|
||||
}
|
||||
for (int i = 0; i < n; i++) free(names[i]);
|
||||
free(names);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0] || !root || !root[0]) return -1;
|
||||
/* 只允许在浏览根内翻册:当前册或相邻册越根一律当「没有相邻册」。 */
|
||||
if (!mr_path_within_root(dir, root)) return 0;
|
||||
|
||||
int r = mr_book_adjacent_ex(dir, delta, out, outsz, archive_pages, ud);
|
||||
if (r != 1) return r;
|
||||
if (!mr_path_within_root(out, root)) {
|
||||
out[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
{
|
||||
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/* logic/library.h — 书库文件管理器模型(纯 C,不依赖 meow_ui.h)。
|
||||
*
|
||||
* 分层契约同 examples/hello:logic 层只含文件系统与纯数据,宿主 gcc 可直接裸测。
|
||||
*
|
||||
* M18 浏览语义(用户要求「完全像文件管理器」):
|
||||
* - 列表 = 当前目录一层的「子目录 + 受支持图片(png/jpg/jpeg/bmp) + 档案册(.cbz/.zip)」;
|
||||
* 目录优先,组内自然序;其它普通文件不列;隐藏项("." 开头)与符号链接跳过;
|
||||
* - **没有**「目录含图=一本书」的判定:不探测、不算页数、不取封面、不拼续读角标;
|
||||
* 点目录就是进目录(空目录也进),点图片从该图开始读,点档案册打开档案册;
|
||||
* - 扫描只做一次 opendir/readdir + 逐项 lstat;副标题(类型 · 大小)在扫描时生成一次,
|
||||
* 渲染期零 IO、零字符串格式化(UI row 回调直接取指针);
|
||||
* - 单目录条目上限 MR_LIB_SCAN_MAX:到上限后若还有条目则截断并置 lib->truncated=1。
|
||||
*
|
||||
* 仍保留的阅读器辅助:mr_library_dir_has_images / mr_book_adjacent(_ex)
|
||||
* (阅读到册末时翻到同父目录的相邻册,与书库列表模型无关)。
|
||||
*/
|
||||
#ifndef MR_LIBRARY_H
|
||||
#define MR_LIBRARY_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_PATH_MAX 512
|
||||
#define MR_NAME_MAX 256
|
||||
#define MR_LIB_MAX_DEPTH 8 /* 兼容旧调用:扫描不递归,该参数被忽略 */
|
||||
|
||||
/* 单目录列出的条目上限(超大目录截断 + UI 提示;可用 -D 覆写便于宿主测试)。 */
|
||||
#ifndef MR_LIB_SCAN_MAX
|
||||
#define MR_LIB_SCAN_MAX 2000
|
||||
#endif
|
||||
|
||||
/* 行类型(列表图标/点击行为由 UI 按此分派)。 */
|
||||
#define MR_LIB_KIND_DIR 0 /* 子目录:点=进入 */
|
||||
#define MR_LIB_KIND_IMAGE 1 /* 受支持图片:点=从该图开始阅读 */
|
||||
#define MR_LIB_KIND_ARCHIVE 2 /* .cbz/.zip 档案册:点=打开档案册 */
|
||||
|
||||
typedef struct mr_library_entry {
|
||||
char name[MR_NAME_MAX]; /* 文件名/目录名(行主标题) */
|
||||
char path[MR_PATH_MAX]; /* 完整路径 */
|
||||
char subtitle[64]; /* 扫描时预生成一次:「文件夹」/「图片 · 1.2 MB」/「档案册 · 1.2 MB」 */
|
||||
long long size; /* 普通文件字节数;目录=0 */
|
||||
int kind; /* MR_LIB_KIND_* */
|
||||
} mr_library_entry;
|
||||
|
||||
typedef struct mr_library {
|
||||
mr_library_entry *items;
|
||||
int count; /* 当前层列出的条目数 */
|
||||
int cap;
|
||||
int truncated; /* 1=达到 MR_LIB_SCAN_MAX 被截断 */
|
||||
} mr_library;
|
||||
|
||||
/* 行数据(纯内存合成;UI 的每帧 src 回调直接取用,零 IO、零格式化)。 */
|
||||
typedef struct mr_library_row {
|
||||
const char *title; /* 主标题 */
|
||||
const char *subtitle; /* 副标题(预生成,永不为 NULL) */
|
||||
int kind; /* MR_LIB_KIND_* */
|
||||
int disabled; /* 1=提示行(不可点,如截断标注) */
|
||||
int index; /* 对应 mr_library.items 下标;提示行=-1 */
|
||||
} mr_library_row;
|
||||
|
||||
/* 扩展名判断:png/jpg/jpeg/bmp(大小写不敏感)。1=受支持图片。 */
|
||||
int mr_path_is_image(const char *name);
|
||||
|
||||
/* 档案册扩展名判断:cbz / zip(大小写不敏感)。1=档案册。 */
|
||||
int mr_path_is_archive(const char *name);
|
||||
|
||||
/* 自然排序比较:数字段按数值比(page2 < page10),数字相等时前导零少的在前;
|
||||
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序(结果确定,无抖动)。 */
|
||||
int mr_natural_cmp(const char *a, const char *b);
|
||||
|
||||
/* 扫描 dir 的**直接子项**(单层):子目录 + 受支持图片 + 档案册,写入 lib(先释放旧内容)。
|
||||
* 返回条目数(>=0);dir 不可读/不存在/不是目录返回 -1(lib 仍为空且可安全 free)。
|
||||
* max_depth 仅为兼容旧调用保留,已忽略(不再递归)。 */
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth);
|
||||
|
||||
/* 释放条目数组并清零(可重复调用)。 */
|
||||
void mr_library_free(mr_library *lib);
|
||||
|
||||
/* 取父目录路径(去掉末尾 '/' 后截断最后一段)。根 "/" 返回 "/"。
|
||||
* 成功 0;参数非法/输出缓冲不足 -1。纯路径工具,不做根目录限制(限制在 UI 导航层)。 */
|
||||
int mr_path_parent(const char *path, char *out, int outsz);
|
||||
|
||||
/* 行合成(纯内存、零 IO、O(1);UI 线程每帧调用):idx 越界返回 0。
|
||||
* idx==count 且 lib->truncated 时输出末尾提示行(disabled=1)。
|
||||
* 返回的 title/subtitle 指向 lib 内部或函数内静态缓冲(截断提示行),调用方不要长期持有。 */
|
||||
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out);
|
||||
|
||||
/* path 是否落在浏览根内(等于 root 或以 "root/" 开头)。1=在内;0=越界/参数非法。
|
||||
* 根锁定用:书库导航与阅读器「相邻册」都不得越过 SD 卡根。 */
|
||||
int mr_path_within_root(const char *path, const char *root);
|
||||
|
||||
/* 目录是否**直接**含受支持图片。1=有;0=没有/不可读。
|
||||
* 仅用于阅读器的「相邻册」判定,不再是书库行语义。 */
|
||||
int mr_library_dir_has_images(const char *dir);
|
||||
|
||||
/* 册间衔接(M7):同父目录下按自然序排列的「直接含图」兄弟册。
|
||||
* delta=+1 下一册 / -1 上一册。1=找到并写入 out 完整路径;
|
||||
* 0=没有相邻册(当前册是首/末册,或当前册不在兄弟表中);-1=参数非法。
|
||||
* 语义:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。
|
||||
* 只算文件夹册(直接含图目录)——M7 口径不变。 */
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz);
|
||||
|
||||
/* 册间衔接 + 档案册(M11):兄弟表 = 「直接含图目录」∪「含图片页的 .cbz/.zip」,
|
||||
* 两者同列按名称自然序混排(跨册衔接按同父目录自然序)。
|
||||
* archive_pages 传 NULL 时与 mr_book_adjacent 完全等价。 */
|
||||
typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud);
|
||||
|
||||
/* 册间衔接(根锁定版):在 mr_book_adjacent_ex 之上拒绝浏览根之外的相邻册。
|
||||
* dir 必须在 root 内;找到的相邻册也必须在 root 内,否则按「没有相邻册」返回 0。
|
||||
* 1=找到并写入 out;0=没有相邻册或越根被拒(out 为空串);-1=参数非法。
|
||||
* 阅读器用它保证「读到末页翻册」不会跳到 SD 卡根之外(例如挂载点以上的其它目录)。 */
|
||||
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
char *out, int outsz);
|
||||
|
||||
#ifdef MR_LIBRARY_TEST_HOOKS
|
||||
/* 宿主测试用 IO 计数(scan/dir_has_images 的 opendir/readdir/lstat 次数;线程安全不保证)。 */
|
||||
typedef struct mr_library_io_stats {
|
||||
long opendirs;
|
||||
long readdirs;
|
||||
long lstats;
|
||||
} mr_library_io_stats;
|
||||
|
||||
void mr_library_test_io_reset(void);
|
||||
void mr_library_test_io_get(mr_library_io_stats *out);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_LIBRARY_H */
|
||||
@@ -0,0 +1,211 @@
|
||||
/* logic/pin.c — APP 启动 PIN:加盐迭代哈希 + meow_config 持久化(纯 C)。
|
||||
*
|
||||
* 设计取舍(见 pin.h 头注释):PIN 防随手翻看,不是安全边界。本文件不引入加密库,
|
||||
* 只做「每次设置新随机盐 + FNV-1a 迭代 1000 轮」的轻量哈希,避免明文 PIN 直接躺在
|
||||
* 配置里。配置落 TF 数据目录,卡被拔走即可读/改——这是产品口径接受的局限。
|
||||
*
|
||||
* 依赖面:只 include include/meow_ui_config.h(get/set/remove/int 声明),不 include
|
||||
* meow_ui.h、不碰 UI,宿主 gcc 可直接编译 + 测试存根替换配置层。
|
||||
*/
|
||||
#include "pin.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define PIN_KEY_ENABLED "pin.enabled"
|
||||
#define PIN_KEY_HASH "pin.hash"
|
||||
#define PIN_KEY_SALT "pin.salt"
|
||||
|
||||
#define PIN_HEX_LEN 16 /* 64-bit 摘要/盐的 hex 字符数 */
|
||||
#define PIN_HEX_BUF 17 /* 16 + NUL */
|
||||
#define PIN_ROUNDS 1000 /* 迭代轮数(防直接反查,不防暴力枚举) */
|
||||
#define PIN_SALT_BYTES 8 /* 8 字节盐 → 16 位 hex */
|
||||
|
||||
/* ---------- 哈希 ---------- */
|
||||
|
||||
/* FNV-1a(64-bit)逐段吸收,返回新状态。 */
|
||||
static uint64_t fnv1a_update(uint64_t h, const void *data, size_t n)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)data;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= (uint64_t)p[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* pin → 16 位 hex 摘要:FNV-1a(salt, pin) 起手,再做 PIN_ROUNDS 轮
|
||||
* 「上一轮摘要(大端 8 字节)+ 盐」的 FNV-1a。同一 PIN 每次设置因盐不同而摘要不同。 */
|
||||
static void pin_digest(const char *pin, const char *salt, char out[PIN_HEX_BUF])
|
||||
{
|
||||
uint64_t h = 1469598103934665603ULL; /* FNV-1a 64-bit offset basis */
|
||||
h = fnv1a_update(h, salt, strlen(salt));
|
||||
h = fnv1a_update(h, pin, strlen(pin));
|
||||
for (int r = 0; r < PIN_ROUNDS; r++) {
|
||||
unsigned char be[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
be[i] = (unsigned char)((h >> (8 * (7 - i))) & 0xFF);
|
||||
h = fnv1a_update(h, be, sizeof be);
|
||||
h = fnv1a_update(h, salt, strlen(salt));
|
||||
}
|
||||
snprintf(out, PIN_HEX_BUF, "%016llx", (unsigned long long)h);
|
||||
}
|
||||
|
||||
/* 生成 16 位 hex 随机盐:优先 /dev/urandom;无设备时用时间/pid/地址/计数器做降级熵
|
||||
* (宿主测试与裁剪系统仍可用——降级盐的强度如实标注:不构成密码学随机)。 */
|
||||
static void pin_gen_salt(char out[PIN_HEX_BUF])
|
||||
{
|
||||
unsigned char b[PIN_SALT_BYTES];
|
||||
int ok = 0;
|
||||
|
||||
FILE *f = fopen("/dev/urandom", "rb");
|
||||
if (f) {
|
||||
ok = (fread(b, 1, sizeof b, f) == sizeof b);
|
||||
fclose(f);
|
||||
}
|
||||
if (!ok) {
|
||||
static uint64_t ctr;
|
||||
uint64_t x = (uint64_t)time(NULL)
|
||||
^ ((uint64_t)clock() << 21)
|
||||
^ ((uint64_t)getpid() << 40)
|
||||
^ (uint64_t)(uintptr_t)&b
|
||||
^ (++ctr * 0x9E3779B97F4A7C15ULL);
|
||||
for (int i = 0; i < PIN_SALT_BYTES; i++)
|
||||
b[i] = (unsigned char)((x >> (8 * i)) & 0xFF);
|
||||
}
|
||||
|
||||
for (int i = 0; i < PIN_SALT_BYTES; i++)
|
||||
snprintf(out + i * 2, 3, "%02x", b[i]);
|
||||
}
|
||||
|
||||
/* ---------- 配置层小包装 ---------- */
|
||||
static int cfg_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
return meow_config_get(key, out, outsz) == MEOW_CFG_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static int cfg_set(const char *key, const char *val)
|
||||
{
|
||||
return meow_config_set(key, val) == MEOW_CFG_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static int is_hex16(const char *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
for (int i = 0; i < PIN_HEX_LEN; i++) {
|
||||
char c = s[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
|
||||
}
|
||||
return s[PIN_HEX_LEN] == '\0';
|
||||
}
|
||||
|
||||
/* ---------- 公共 API ---------- */
|
||||
|
||||
int mr_pin_valid(const char *pin)
|
||||
{
|
||||
if (!pin) return 0;
|
||||
int n = 0;
|
||||
while (pin[n]) {
|
||||
if (pin[n] < '0' || pin[n] > '9') return 0;
|
||||
n++;
|
||||
if (n > MR_PIN_MAX_LEN) return 0;
|
||||
}
|
||||
return n >= MR_PIN_MIN_LEN;
|
||||
}
|
||||
|
||||
int mr_pin_enabled(void)
|
||||
{
|
||||
char hash[PIN_HEX_BUF + 8];
|
||||
char salt[PIN_HEX_BUF + 8];
|
||||
|
||||
if (meow_config_int(PIN_KEY_ENABLED, 0) != 1) return 0;
|
||||
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
|
||||
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
|
||||
return is_hex16(hash) && is_hex16(salt); /* 配置残缺 → fail-closed */
|
||||
}
|
||||
|
||||
int mr_pin_set(const char *pin)
|
||||
{
|
||||
char salt[PIN_HEX_BUF];
|
||||
char hash[PIN_HEX_BUF];
|
||||
char old_salt[PIN_HEX_BUF + 8];
|
||||
int had_old_salt;
|
||||
|
||||
if (!mr_pin_valid(pin)) return MR_PIN_ERR_FORMAT;
|
||||
|
||||
pin_gen_salt(salt);
|
||||
pin_digest(pin, salt, hash);
|
||||
had_old_salt = (cfg_get(PIN_KEY_SALT, old_salt, (int)sizeof old_salt) == 0);
|
||||
|
||||
/* 先写盐与摘要、最后打开 enabled:中途失败只会留下「未启用」,不会出现
|
||||
* enabled=1 但缺少盐/哈希的半残状态。摘要写失败时把盐还原成旧值,使已启用的旧
|
||||
* PIN 仍可校验(否则「新盐+旧哈希」会让新旧 PIN 都验不过)。 */
|
||||
if (cfg_set(PIN_KEY_SALT, salt) != 0) return MR_PIN_ERR_STORE;
|
||||
if (cfg_set(PIN_KEY_HASH, hash) != 0) {
|
||||
if (had_old_salt) cfg_set(PIN_KEY_SALT, old_salt);
|
||||
else meow_config_remove(PIN_KEY_SALT);
|
||||
return MR_PIN_ERR_STORE;
|
||||
}
|
||||
if (cfg_set(PIN_KEY_ENABLED, "1") != 0) {
|
||||
/* 重设时 enabled 本来就是 "1":新盐/哈希已一致生效,按成功返回;
|
||||
* 首次开启时 enabled 仍为 0 → 如实报落盘失败。 */
|
||||
return meow_config_int(PIN_KEY_ENABLED, 0) == 1 ? MR_PIN_OK : MR_PIN_ERR_STORE;
|
||||
}
|
||||
return MR_PIN_OK;
|
||||
}
|
||||
|
||||
int mr_pin_verify(const char *pin)
|
||||
{
|
||||
char hash[PIN_HEX_BUF + 8];
|
||||
char salt[PIN_HEX_BUF + 8];
|
||||
char calc[PIN_HEX_BUF];
|
||||
|
||||
if (!mr_pin_valid(pin)) return 0;
|
||||
if (!mr_pin_enabled()) return 0;
|
||||
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
|
||||
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
|
||||
|
||||
pin_digest(pin, salt, calc);
|
||||
unsigned diff = 0;
|
||||
for (int i = 0; i < PIN_HEX_LEN; i++)
|
||||
diff |= (unsigned)((unsigned char)hash[i] ^ (unsigned char)calc[i]);
|
||||
diff |= (unsigned)(hash[PIN_HEX_LEN] != '\0');
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
int mr_pin_change(const char *old_pin, const char *new_pin)
|
||||
{
|
||||
if (!mr_pin_enabled()) return MR_PIN_ERR_STATE;
|
||||
if (!mr_pin_valid(new_pin)) return MR_PIN_ERR_FORMAT;
|
||||
if (!mr_pin_verify(old_pin)) return MR_PIN_ERR_WRONG;
|
||||
return mr_pin_set(new_pin);
|
||||
}
|
||||
|
||||
int mr_pin_disable(void)
|
||||
{
|
||||
meow_config_remove(PIN_KEY_HASH);
|
||||
meow_config_remove(PIN_KEY_SALT);
|
||||
if (cfg_set(PIN_KEY_ENABLED, "0") != 0) return MR_PIN_ERR_STORE;
|
||||
return MR_PIN_OK;
|
||||
}
|
||||
|
||||
int mr_pin_get_salt(char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (cfg_get(PIN_KEY_SALT, out, outsz) != 0) return 0;
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
int mr_pin_get_hash(char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (cfg_get(PIN_KEY_HASH, out, outsz) != 0) return 0;
|
||||
return out[0] != '\0';
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* logic/pin.h — APP 启动 PIN(纯 C;宿主可裸测)。
|
||||
*
|
||||
* 定位(诚实标注):PIN 只挡「随手翻看」——TF 卡可拔出直读,数据目录明文可改,
|
||||
* 故不引入加密库、不宣称安全边界。存储口径:加盐迭代哈希(FNV-1a ×1000 轮,
|
||||
* 每次设置新随机盐,hex 输出),落在 meow_config_*(App 自有 KV → TF MeowData)。
|
||||
* 盐/哈希只做「不知道 PIN 无法直接重算」的轻量门槛,防不了离线暴力或改配置。
|
||||
*
|
||||
* 配置键(meow_config_*,值均为字符串):
|
||||
* pin.enabled = "1"/"0"
|
||||
* pin.hash = 16 位 hex(64-bit 摘要)
|
||||
* pin.salt = 16 位 hex(每次设置重新生成)
|
||||
* enabled=1 但 hash/salt 缺失或格式不合法时一律按「未启用」处理(fail-closed)。
|
||||
*
|
||||
* 本模块只依赖 include/meow_ui_config.h 的三个声明(get/set/remove/int),不碰 UI;
|
||||
* 宿主测试链 test_pin.c 内的 meow_config_* 存根即可,无需 SDK/设备。
|
||||
*/
|
||||
#ifndef MR_PIN_H
|
||||
#define MR_PIN_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_PIN_MIN_LEN 4 /* 允许的最短位数 */
|
||||
#define MR_PIN_MAX_LEN 8 /* 允许的最长位数 */
|
||||
|
||||
/* 返回码(set/change/disable)。verify 用 1/0 表示对/错,不返回错误码。 */
|
||||
#define MR_PIN_OK 0 /* 成功 */
|
||||
#define MR_PIN_ERR_FORMAT (-1) /* PIN 非法:非 4-8 位纯数字(NULL/空/符号/超长) */
|
||||
#define MR_PIN_ERR_STATE (-2) /* 状态不满足:change 时尚未启用 */
|
||||
#define MR_PIN_ERR_WRONG (-3) /* change 时原 PIN 校验失败 */
|
||||
#define MR_PIN_ERR_STORE (-4) /* meow_config_* 落盘失败(无数据目录/只读/IO) */
|
||||
|
||||
/* 4-8 位纯 ASCII 数字('0'-'9')→ 1;其余(含 NULL、空串、符号、全角数字)→ 0。 */
|
||||
int mr_pin_valid(const char *pin);
|
||||
|
||||
/* 当前是否启用:1=已启用且 hash/salt 完整合法;0=未启用或配置损坏(fail-closed)。 */
|
||||
int mr_pin_enabled(void);
|
||||
|
||||
/* 设置/重设 PIN(生成新盐 + 迭代哈希并立即落盘)。成功即生效(下次启动进锁屏)。
|
||||
* 返回 MR_PIN_OK / MR_PIN_ERR_FORMAT / MR_PIN_ERR_STORE。 */
|
||||
int mr_pin_set(const char *pin);
|
||||
|
||||
/* 校验:1=匹配;0=不匹配/未启用/输入非法(不区分,避免侧信道式提示)。 */
|
||||
int mr_pin_verify(const char *pin);
|
||||
|
||||
/* 修改:先校验 old,再以新盐重设 new。失败时旧 PIN 保持有效(先校验后写盘)。
|
||||
* 返回 MR_PIN_OK / MR_PIN_ERR_STATE / MR_PIN_ERR_FORMAT / MR_PIN_ERR_WRONG / MR_PIN_ERR_STORE。 */
|
||||
int mr_pin_change(const char *old_pin, const char *new_pin);
|
||||
|
||||
/* 关闭 PIN:删 hash/salt 并把 enabled 置 0(幂等;调用方已通过锁屏验证时无需再输 PIN)。
|
||||
* 返回 MR_PIN_OK / MR_PIN_ERR_STORE。 */
|
||||
int mr_pin_disable(void);
|
||||
|
||||
/* 诊断/测试用:读回当前 hex 盐/哈希。返回 1=读成功,0=未设。 */
|
||||
int mr_pin_get_salt(char *out, int outsz);
|
||||
int mr_pin_get_hash(char *out, int outsz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_PIN_H */
|
||||
@@ -0,0 +1,200 @@
|
||||
/* logic/reader.c — 阅读方向状态(默认 RTL;M4/M6 的阅读器直接复用)+ 每册续读状态(M7)。
|
||||
*
|
||||
* M5 评审 P1 修复:方向设置要「下次启动生效」。最小持久化(不新增任何方向状态):
|
||||
* - mr_reader_set_direction 成功切换后 best-effort 写 meow_config("reader.dir","rtl"/"ltr");
|
||||
* 写失败只打 WARN,不阻断切换(本次运行仍生效);
|
||||
* - mr_reader_get_direction 首次调用惰性读一次配置恢复;无配置/值非法 → 默认 RTL;
|
||||
* 之后走纯内存快路径(不再读配置)。
|
||||
* M7:阅读模式(翻页/条漫)与每册 last page 同为 meow_config 小键(reader.mode/page.<册标识>,
|
||||
* 册标识=规范化目录路径的 FNV-1a64);书库行角标经 mr_cont_badge 读取同一份状态。
|
||||
* M33:mr_cont_resume_page 给「进入目录后的顶栏续读按钮」做可用性判定与页码夹持
|
||||
* (保存页超出当前页数 → 钳到末页;空目录/无图片 → 无入口)。
|
||||
* 依赖面:只 include 配置/日志两个模块头(不含 UI);宿主测试用
|
||||
* test/settings_reader_stub.c 顶替 meow_config_* 与 meow_log。
|
||||
*/
|
||||
#include "reader.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
#include "include/meow_ui_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MR_DIR_CFG_KEY "reader.dir"
|
||||
#define MR_DIR_CFG_LTR "ltr"
|
||||
#define MR_DIR_CFG_RTL "rtl"
|
||||
|
||||
static int g_direction = MR_DIR_RTL;
|
||||
static int g_restored; /* 0=尚未从配置恢复(首次 get/set 时读一次) */
|
||||
|
||||
static void reader_restore_once(void)
|
||||
{
|
||||
char buf[8];
|
||||
|
||||
if (g_restored) return;
|
||||
g_restored = 1; /* 无配置也只读一次:之后纯内存 */
|
||||
if (meow_config_get(MR_DIR_CFG_KEY, buf, sizeof buf) != MEOW_CFG_OK) return;
|
||||
if (strcmp(buf, MR_DIR_CFG_LTR) == 0) g_direction = MR_DIR_LTR;
|
||||
else if (strcmp(buf, MR_DIR_CFG_RTL) == 0) g_direction = MR_DIR_RTL;
|
||||
/* 其它值:忽略,保持默认 RTL */
|
||||
}
|
||||
|
||||
int mr_reader_get_direction(void)
|
||||
{
|
||||
reader_restore_once();
|
||||
return g_direction;
|
||||
}
|
||||
|
||||
void mr_reader_set_direction(int dir)
|
||||
{
|
||||
if (dir != MR_DIR_RTL && dir != MR_DIR_LTR) return;
|
||||
|
||||
reader_restore_once(); /* 先恢复:避免「没 get 过就 set」把配置里的旧方向当默认值 */
|
||||
if (g_direction == dir) return; /* 无变化不写盘 */
|
||||
g_direction = dir;
|
||||
|
||||
if (meow_config_set(MR_DIR_CFG_KEY, dir == MR_DIR_LTR ? MR_DIR_CFG_LTR : MR_DIR_CFG_RTL)
|
||||
!= MEOW_CFG_OK)
|
||||
meow_log(MEOW_LOG_WARN, "reader.dir: save failed (best-effort, kept in memory)");
|
||||
}
|
||||
|
||||
#ifdef MR_READER_TEST_HOOKS
|
||||
void mr_reader_test_reset(void)
|
||||
{
|
||||
g_direction = MR_DIR_RTL;
|
||||
g_restored = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* 阅读模式 + 每册续读状态(M7) */
|
||||
/* ============================================================ */
|
||||
int mr_mode_valid(int mode)
|
||||
{
|
||||
return mode == MR_MODE_PAGED || mode == MR_MODE_WEBTOON;
|
||||
}
|
||||
|
||||
const char *mr_mode_name(int mode)
|
||||
{
|
||||
return mode == MR_MODE_WEBTOON ? "webtoon" : "paged";
|
||||
}
|
||||
|
||||
int mr_mode_parse(const char *s)
|
||||
{
|
||||
if (!s) return -1;
|
||||
if (strcmp(s, "paged") == 0) return MR_MODE_PAGED;
|
||||
if (strcmp(s, "webtoon") == 0) return MR_MODE_WEBTOON;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 目录规范化:去掉末尾 '/'(根 "/" 保留),保证同一册跨调用/跨启动得到同一键。 */
|
||||
static void cont_norm_dir(const char *dir, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0]) return;
|
||||
int n = (int)strlen(dir);
|
||||
while (n > 1 && dir[n - 1] == '/') n--;
|
||||
if (n >= outsz) n = outsz - 1;
|
||||
memcpy(out, dir, (size_t)n);
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
static unsigned long long cont_fnv1a64(const char *s)
|
||||
{
|
||||
unsigned long long h = 1469598103934665603ULL;
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
h ^= (unsigned long long)*p;
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
int mr_cont_key(const char *dir, const char *field, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0] || !field || !field[0]) return -1;
|
||||
char norm[MR_CONT_PATH_MAX];
|
||||
cont_norm_dir(dir, norm, (int)sizeof norm);
|
||||
if (!norm[0]) return -1;
|
||||
int n = snprintf(out, (size_t)outsz, "reader.%s.%016llx", field, cont_fnv1a64(norm));
|
||||
if (n <= 0 || n >= outsz) { out[0] = '\0'; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cont_get_page(const char *dir, int def)
|
||||
{
|
||||
char key[64];
|
||||
char buf[24];
|
||||
if (mr_cont_key(dir, "page", key, (int)sizeof key) != 0) return def;
|
||||
if (meow_config_get(key, buf, (int)sizeof buf) != MEOW_CFG_OK) return def;
|
||||
char *end = NULL;
|
||||
long v = strtol(buf, &end, 10);
|
||||
if (end == buf || (end && *end != '\0')) return def;
|
||||
if (v < 0 || v > MR_CONT_PAGE_MAX) return def;
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
void mr_cont_set_page(const char *dir, int page)
|
||||
{
|
||||
char key[64];
|
||||
char val[24];
|
||||
if (page < 0) page = 0;
|
||||
if (page > MR_CONT_PAGE_MAX) page = MR_CONT_PAGE_MAX;
|
||||
if (mr_cont_key(dir, "page", key, (int)sizeof key) != 0) return;
|
||||
snprintf(val, sizeof val, "%d", page);
|
||||
if (meow_config_set(key, val) != MEOW_CFG_OK)
|
||||
meow_log(MEOW_LOG_WARN, "reader: save page failed (best-effort)");
|
||||
}
|
||||
|
||||
int mr_cont_get_mode(const char *dir, int def)
|
||||
{
|
||||
char key[64];
|
||||
char buf[16];
|
||||
if (!mr_mode_valid(def)) def = MR_MODE_PAGED;
|
||||
if (mr_cont_key(dir, "mode", key, (int)sizeof key) != 0) return def;
|
||||
if (meow_config_get(key, buf, (int)sizeof buf) != MEOW_CFG_OK) return def;
|
||||
int m = mr_mode_parse(buf);
|
||||
return m >= 0 ? m : def;
|
||||
}
|
||||
|
||||
void mr_cont_set_mode(const char *dir, int mode)
|
||||
{
|
||||
char key[64];
|
||||
if (!mr_mode_valid(mode)) return;
|
||||
if (mr_cont_key(dir, "mode", key, (int)sizeof key) != 0) return;
|
||||
if (meow_config_set(key, mr_mode_name(mode)) != MEOW_CFG_OK)
|
||||
meow_log(MEOW_LOG_WARN, "reader: save mode failed (best-effort)");
|
||||
}
|
||||
|
||||
void mr_cont_save(const char *dir, int page, int mode)
|
||||
{
|
||||
if (!dir || !dir[0]) return;
|
||||
mr_cont_set_page(dir, page);
|
||||
mr_cont_set_mode(dir, mode);
|
||||
}
|
||||
|
||||
int mr_cont_badge(const char *dir, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
int page = mr_cont_get_page(dir, -1);
|
||||
if (page <= 0) return 0;
|
||||
snprintf(out, (size_t)outsz, "续读 P%d", page + 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_cont_resume_page(const char *dir, int pages)
|
||||
{
|
||||
int page = mr_cont_get_page(dir, -1);
|
||||
if (page < 0) return -1;
|
||||
if (pages == 0) return -1; /* 空目录/无图片:没有可续读的页 */
|
||||
if (pages < 0) return page; /* 页数未知(列表截断):上界交给阅读器钳制 */
|
||||
if (page >= pages) {
|
||||
meow_log(MEOW_LOG_INFO, "reader: resume page %d beyond %d pages (%s), clamp to last",
|
||||
page + 1, pages, dir ? dir : "");
|
||||
return pages - 1;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* logic/reader.h — 阅读器纯逻辑接缝(M4 填充实现,M3 先落状态 API;M7 追加模式与每册续读状态)。
|
||||
*
|
||||
* 阅读方向默认 RTL(日式漫画从右往左翻);设置页/阅读器可切 LTR。
|
||||
* 阅读模式(翻页/条漫)与每册 last page 走 meow_config 持久化,键含册标识。
|
||||
* 本模块保持纯 C(不 include meow_ui.h),宿主可直接测。
|
||||
*/
|
||||
#ifndef MR_READER_H
|
||||
#define MR_READER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_DIR_RTL 1 /* 默认:右到左(日漫) */
|
||||
#define MR_DIR_LTR 0 /* 左到右 */
|
||||
|
||||
int mr_reader_get_direction(void);
|
||||
/* 只接受 MR_DIR_RTL / MR_DIR_LTR;其它值忽略(保持原方向)。 */
|
||||
void mr_reader_set_direction(int dir);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 阅读模式(M7):左右翻页 / 条漫竖向连续滚动 */
|
||||
/* ============================================================ */
|
||||
#define MR_MODE_PAGED 0 /* 左右翻页(默认) */
|
||||
#define MR_MODE_WEBTOON 1 /* 条漫:垂直连续滚动 */
|
||||
|
||||
int mr_mode_valid(int mode);
|
||||
const char *mr_mode_name(int mode); /* "paged" / "webtoon"(配置值;非法按 paged) */
|
||||
int mr_mode_parse(const char *s); /* -1=未知 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* 每册续读状态(last page + mode,meow_config 持久化) */
|
||||
/* ------------------------------------------------------------
|
||||
* 键 = reader.<field>.<FNV-1a64(规范化目录路径)>:同一册(同一目录)跨启动恢复,
|
||||
* 不同册互不影响;目录改名/移动视为新册。写失败只 WARN(best-effort,不阻断阅读)。
|
||||
* 依赖:只 include 配置/日志两个模块头,宿主测试用 settings_reader_stub 顶替。
|
||||
* ============================================================ */
|
||||
#define MR_CONT_PATH_MAX 512 /* 册路径规范化的本地缓冲(同 MR_PATH_MAX 口径) */
|
||||
#define MR_CONT_PAGE_MAX 1000000 /* 配置里 last page 的合理上限(防坏值) */
|
||||
|
||||
int mr_cont_key(const char *dir, const char *field, char *out, int outsz);
|
||||
int mr_cont_get_mode(const char *dir, int def);
|
||||
void mr_cont_set_mode(const char *dir, int mode);
|
||||
int mr_cont_get_page(const char *dir, int def);
|
||||
void mr_cont_set_page(const char *dir, int page);
|
||||
/* 一次写 page+mode(尽最大努力,两键分别尝试)。 */
|
||||
void mr_cont_save(const char *dir, int page, int mode);
|
||||
/* 书库行续读角标:1=写入 "续读 P5"(page>0 才有记录);0=无。 */
|
||||
int mr_cont_badge(const char *dir, char *out, int outsz);
|
||||
|
||||
/* 目录内「续读」入口的可用页(M33:按钮在**进入目录后**的顶栏,不在上层列表行)。
|
||||
* pages > 0:该目录当前图片页数;保存页 >= pages(文件被删/目录变短)→ 钳到 pages-1
|
||||
* 并 INFO 日志注明,保证「续读」永远落在有效页;
|
||||
* pages == 0:空目录/无图片 → 恒 -1(入口不显示);
|
||||
* pages < 0:页数未知(书库列表被 MR_LIB_SCAN_MAX 截断)→ 有记录就原样返回,
|
||||
* 上界交给阅读器 open_book 的 mr_nav_clamp_page 钳制。
|
||||
* 返回 -1 = 当前目录没有可用的续读记录(不显示入口)。 */
|
||||
int mr_cont_resume_page(const char *dir, int pages);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_READER_H */
|
||||
@@ -0,0 +1,416 @@
|
||||
/* logic/reader/archive_source.c — 档案册(CBZ/zip 当一本书):页源契约的第二种实现。
|
||||
*
|
||||
* 契约、条目过滤与缓存纪律见 page_source.h / page_cache.h。实现要点:
|
||||
* - 页表在 init 时一次建好(mz_open 只读 central directory;只看名字,不解压);
|
||||
* - open(i) 才解压单条目:mz_read_alloc → page_cache 落盘 → 注入的解码回调按路径解码。
|
||||
* 因此翻页/条漫两套消费代码与目录册完全一致(meta 走原始像素尺寸,open 走解码);
|
||||
* - meta(i) 只读该条目的**前 4KB**(mz_read_prefix)取图片头(不落缓存),结果缓存在
|
||||
* meta_w/meta_h;M32 前是整条目解压,500 页排版 = 500 次全量解压;
|
||||
* - 单个 mr_zip_book 的 mz_read* 由自带的互斥量串行化(解码线程与主线程 meta 会碰面)。
|
||||
*
|
||||
* 线程:count/page_name 只读页表,可任意线程;open 可由解码线程调用;meta 按 M6
|
||||
* 用法在主线程调用(条漫排版/当前页几何)。两个 mr_zip_book 实例互不影响,
|
||||
* 解压缓存(page_cache)是进程级共享的。
|
||||
*/
|
||||
#include "logic/reader/page_source.h"
|
||||
#include "logic/reader/page_cache.h"
|
||||
#include "logic/zip/zip.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条目过滤:图片后缀 + 路径安全 + 非隐藏 + 非目录 */
|
||||
/* ============================================================ */
|
||||
static int has_control(const char *name, unsigned len)
|
||||
{
|
||||
for (unsigned i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)name[i];
|
||||
if (c < 0x20 || c == 0x7F) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 逐段检查:拒绝 ".." 段与隐藏段("._resource" 之类不参与成册)。 */
|
||||
static int components_safe(const char *name)
|
||||
{
|
||||
const char *p = name;
|
||||
while (*p) {
|
||||
const char *end = strchr(p, '/');
|
||||
size_t n = end ? (size_t)(end - p) : strlen(p);
|
||||
if (n == 2 && p[0] == '.' && p[1] == '.') return 0;
|
||||
if (n > 0 && p[0] == '.') return 0;
|
||||
if (!end) break;
|
||||
p = end + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_zip_entry_is_page(const char *name, unsigned name_len)
|
||||
{
|
||||
if (!name || name_len == 0 || name_len >= MR_ZIP_MAX_ENTRY_NAME) return 0;
|
||||
if (strlen(name) != name_len) return 0; /* 内嵌 NUL:拒绝 */
|
||||
if (name[0] == '/' || name[0] == '\\') return 0; /* 绝对路径 */
|
||||
if (name_len >= 2 && name[1] == ':') return 0; /* Windows 盘符 */
|
||||
if (strchr(name, '\\')) return 0; /* 反斜杠分隔符:一律拒绝 */
|
||||
if (has_control(name, name_len)) return 0;
|
||||
if (name[name_len - 1] == '/') return 0; /* 目录条目 */
|
||||
if (!components_safe(name)) return 0;
|
||||
return mr_path_is_image(name);
|
||||
}
|
||||
|
||||
/* 页表排序:跨子目录按完整条目名自然序(中文按 UTF-8 字节序,数字段按数值)。 */
|
||||
typedef struct page_ref {
|
||||
const char *name;
|
||||
int eidx;
|
||||
} page_ref;
|
||||
|
||||
static int page_ref_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const page_ref *a = (const page_ref *)pa;
|
||||
const page_ref *b = (const page_ref *)pb;
|
||||
int c = mr_natural_cmp(a->name, b->name);
|
||||
if (c != 0) return c;
|
||||
return a->eidx < b->eidx ? -1 : (a->eidx > b->eidx ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 生命周期 */
|
||||
/* ============================================================ */
|
||||
static int entry_is_page(const mz_entry *e)
|
||||
{
|
||||
if (!e || mz_entry_is_dir(e)) return 0;
|
||||
if (e->flags & 0x0001u) return 0; /* 加密条目:读不了,不列 */
|
||||
if (!mr_zip_entry_is_page(e->name, e->name_len)) return 0;
|
||||
if (e->uncomp_size == 0) return 0;
|
||||
if (e->uncomp_size > (uint64_t)MR_ZIP_PAGE_MAX_BYTES) return 0; /* zip 炸弹 */
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mr_zip_book_free(mr_zip_book *zb)
|
||||
{
|
||||
if (!zb) return;
|
||||
if (zb->arch) {
|
||||
mz_close((mz_archive *)zb->arch);
|
||||
zb->arch = NULL;
|
||||
}
|
||||
if (zb->lock) {
|
||||
pthread_mutex_destroy((pthread_mutex_t *)zb->lock);
|
||||
free(zb->lock);
|
||||
zb->lock = NULL;
|
||||
}
|
||||
free(zb->order);
|
||||
zb->order = NULL;
|
||||
free(zb->meta_w);
|
||||
zb->meta_w = NULL;
|
||||
free(zb->meta_h);
|
||||
zb->meta_h = NULL;
|
||||
zb->count = 0;
|
||||
}
|
||||
|
||||
int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (!zb) return -1;
|
||||
memset(zb, 0, sizeof *zb);
|
||||
if (!path || !path[0]) return -1;
|
||||
if (strlen(path) >= sizeof zb->path) return -1;
|
||||
if (maxdim <= 0) maxdim = 720;
|
||||
|
||||
mz_archive *z = NULL;
|
||||
if (mz_open(path, &z) != MZ_OK || !z) return -1;
|
||||
|
||||
pthread_mutex_t *lk = (pthread_mutex_t *)malloc(sizeof *lk);
|
||||
if (!lk) {
|
||||
mz_close(z);
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_init(lk, NULL);
|
||||
|
||||
snprintf(zb->path, sizeof zb->path, "%s", path);
|
||||
zb->arch = z;
|
||||
zb->lock = lk;
|
||||
zb->maxdim = maxdim;
|
||||
zb->load = load;
|
||||
zb->unload = unload;
|
||||
zb->ud = ud;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) {
|
||||
zb->mtime = (long long)st.st_mtime;
|
||||
zb->size = (long long)st.st_size;
|
||||
}
|
||||
|
||||
int n = mz_count(z);
|
||||
page_ref *refs = NULL;
|
||||
int nref = 0, cap = 0;
|
||||
for (int i = 0; i < n && nref < MR_BOOK_MAX_PAGES; i++) {
|
||||
const mz_entry *e = mz_entry_at(z, i);
|
||||
if (!entry_is_page(e)) continue;
|
||||
if (nref >= cap) {
|
||||
int nc = cap ? cap * 2 : 32;
|
||||
page_ref *nr = (page_ref *)realloc(refs, (size_t)nc * sizeof *nr);
|
||||
if (!nr) {
|
||||
free(refs);
|
||||
mr_zip_book_free(zb);
|
||||
return -1;
|
||||
}
|
||||
refs = nr;
|
||||
cap = nc;
|
||||
}
|
||||
refs[nref].name = e->name;
|
||||
refs[nref].eidx = i;
|
||||
nref++;
|
||||
}
|
||||
if (nref > 1) qsort(refs, (size_t)nref, sizeof *refs, page_ref_cmp);
|
||||
if (nref > 0) {
|
||||
zb->order = (int *)malloc((size_t)nref * sizeof *zb->order);
|
||||
if (!zb->order) {
|
||||
free(refs);
|
||||
mr_zip_book_free(zb);
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < nref; i++) zb->order[i] = refs[i].eidx;
|
||||
}
|
||||
free(refs);
|
||||
zb->count = nref;
|
||||
return nref;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页 → 缓存文件 */
|
||||
/* ============================================================ */
|
||||
static const mz_entry *page_entry(mr_zip_book *zb, int index)
|
||||
{
|
||||
if (!zb || !zb->arch || !zb->order) return NULL;
|
||||
if (index < 0 || index >= zb->count) return NULL;
|
||||
return mz_entry_at((mz_archive *)zb->arch, zb->order[index]);
|
||||
}
|
||||
|
||||
int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
|
||||
const mz_entry *e = page_entry(zb, index);
|
||||
if (!e) return -1;
|
||||
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
if (mr_cache_path(zb->path, zb->mtime, zb->size, e->name, index,
|
||||
path, (int)sizeof path) != 0)
|
||||
return -1;
|
||||
if (mr_cache_touch(path)) {
|
||||
snprintf(out, (size_t)outsz, "%s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
int rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK || !buf) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
int ok = mr_cache_store(path, buf, len);
|
||||
free(buf);
|
||||
if (!ok) return -1;
|
||||
snprintf(out, (size_t)outsz, "%s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
const mz_entry *e = page_entry((mr_zip_book *)zb, index);
|
||||
if (!e) return;
|
||||
snprintf(out, (size_t)outsz, "%s", e->name);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 书库辅助:数页 / 取封面 */
|
||||
/* ============================================================ */
|
||||
int mr_zip_count_pages(const char *path)
|
||||
{
|
||||
if (!path || !path[0]) return 0;
|
||||
mz_archive *z = NULL;
|
||||
if (mz_open(path, &z) != MZ_OK || !z) return 0;
|
||||
int n = mz_count(z), pages = 0;
|
||||
for (int i = 0; i < n && pages < MR_BOOK_MAX_PAGES; i++)
|
||||
if (entry_is_page(mz_entry_at(z, i))) pages++;
|
||||
mz_close(z);
|
||||
return pages;
|
||||
}
|
||||
|
||||
int mr_zip_cover_path(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (!path || !path[0]) return 0;
|
||||
|
||||
mr_zip_book zb;
|
||||
int n = mr_zip_book_init(&zb, path, 0, NULL, NULL, NULL);
|
||||
int ok = 0;
|
||||
if (n > 0) ok = (mr_zip_book_page_path(&zb, 0, out, outsz) == 0);
|
||||
mr_zip_book_free(&zb);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* mr_book_ops 适配器(档案册) */
|
||||
/* ============================================================ */
|
||||
static int zip_ops_count(void *ctx)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
return zb ? zb->count : 0;
|
||||
}
|
||||
|
||||
/* meta 只读条目**前缀**(≤4KB)取图片头(M32):改前是 mz_read_alloc 整条目解压后再探
|
||||
* (500 页排版 = 500 次全量解压)。PNG IHDR 33 字节、JPEG SOF 在头几百字节、BMP 在
|
||||
* 54 字节 + 调色板内,4KB 足够;前缀探测失败且确实被截断时才回退整条目读(罕见/病态文件)。 */
|
||||
#define MR_ZIP_META_PREFIX 4096
|
||||
|
||||
static int zip_meta_probe_prefix(mr_zip_book *zb, const mz_entry *e, int *w, int *h)
|
||||
{
|
||||
unsigned char head[MR_ZIP_META_PREFIX];
|
||||
size_t head_len = 0;
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
int rc = mz_read_prefix((mz_archive *)zb->arch, e, head, sizeof head, &head_len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK) return 0;
|
||||
if (mr_probe_size_mem(head, head_len, w, h)) return 1;
|
||||
if (head_len < sizeof head) return 0; /* 整个条目都读到了还探不出 → 坏头 */
|
||||
|
||||
/* 前缀被截断且探不出尺寸(病态长的头部):回退整条目读,保持旧语义。 */
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK || !buf) {
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
int known = mr_probe_size_mem(buf, len, w, h);
|
||||
free(buf);
|
||||
return known;
|
||||
}
|
||||
|
||||
/* meta 按需探测图片头;结果缓存,每页只读一次(前缀 4KB,不再整页解压)。主线程调用(M6 用法)。 */
|
||||
static int zip_ops_meta(void *ctx, int index, int *w, int *h)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!zb || index < 0 || index >= zb->count) return 0;
|
||||
|
||||
if (!zb->meta_w) {
|
||||
int *mw = (int *)calloc((size_t)zb->count, sizeof *mw);
|
||||
int *mh = (int *)calloc((size_t)zb->count, sizeof *mh);
|
||||
if (!mw || !mh) {
|
||||
free(mw);
|
||||
free(mh);
|
||||
return 0;
|
||||
}
|
||||
zb->meta_w = mw;
|
||||
zb->meta_h = mh;
|
||||
}
|
||||
if (zb->meta_w[index] > 0 && zb->meta_h[index] > 0) {
|
||||
if (w) *w = zb->meta_w[index];
|
||||
if (h) *h = zb->meta_h[index];
|
||||
return 1;
|
||||
}
|
||||
|
||||
const mz_entry *e = page_entry(zb, index);
|
||||
if (!e) return 0;
|
||||
|
||||
int pw = 0, ph = 0;
|
||||
if (!zip_meta_probe_prefix(zb, e, &pw, &ph)) return 0;
|
||||
|
||||
zb->meta_w[index] = pw;
|
||||
zb->meta_h[index] = ph;
|
||||
if (w) *w = pw;
|
||||
if (h) *h = ph;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static meow_img *zip_ops_open(void *ctx, int index)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (!zb || !zb->load) return NULL;
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
if (mr_zip_book_page_path(zb, index, path, (int)sizeof path) != 0) return NULL;
|
||||
return zb->load(path, zb->ud, zb->maxdim);
|
||||
}
|
||||
|
||||
static void zip_ops_close(void *ctx, meow_img *img)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (zb && zb->unload && img) zb->unload(img);
|
||||
}
|
||||
|
||||
static void zip_ops_destroy(void *ctx)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (zb) mr_zip_book_free(zb);
|
||||
}
|
||||
|
||||
static const mr_book_ops g_zip_ops = {
|
||||
zip_ops_count, zip_ops_meta, zip_ops_open, zip_ops_close, zip_ops_destroy,
|
||||
};
|
||||
|
||||
void mr_zip_book_bind(mr_book *b, mr_zip_book *zb)
|
||||
{
|
||||
if (!b) return;
|
||||
b->ops = zb ? &g_zip_ops : NULL;
|
||||
b->ctx = zb;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 统一开册 */
|
||||
/* ============================================================ */
|
||||
int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (b) {
|
||||
b->ops = NULL;
|
||||
b->ctx = NULL;
|
||||
}
|
||||
if (!b || !h) return -1;
|
||||
memset(h, 0, sizeof *h);
|
||||
if (!path || !path[0]) return -1;
|
||||
|
||||
int n;
|
||||
/* 目录名也可能以 .cbz 结尾(罕见):只有普通文件才当档案册。 */
|
||||
int is_archive = mr_path_is_archive(path);
|
||||
if (is_archive) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) is_archive = 0;
|
||||
}
|
||||
if (is_archive) {
|
||||
n = mr_zip_book_init(&h->u.zip, path, maxdim, load, unload, ud);
|
||||
if (n < 0) return -1;
|
||||
h->kind = MR_BOOK_KIND_ZIP;
|
||||
if (n > 0) mr_zip_book_bind(b, &h->u.zip);
|
||||
} else {
|
||||
n = mr_dir_book_init(&h->u.dir, path, maxdim, load, unload, ud);
|
||||
if (n < 0) return -1;
|
||||
h->kind = MR_BOOK_KIND_DIR;
|
||||
if (n > 0) mr_dir_book_bind(b, &h->u.dir);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void mr_book_close_auto(mr_book *b, mr_book_handle *h)
|
||||
{
|
||||
if (b) mr_book_destroy(b);
|
||||
if (!h) return;
|
||||
if (h->kind == MR_BOOK_KIND_ZIP) mr_zip_book_free(&h->u.zip);
|
||||
else if (h->kind == MR_BOOK_KIND_DIR) mr_dir_book_free(&h->u.dir);
|
||||
memset(h, 0, sizeof *h);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/* logic/reader/nav.c — 阅读导航纯逻辑实现(见 nav.h 的方向语义说明)。 */
|
||||
#include "logic/reader/nav.h"
|
||||
#include "logic/reader/page_source.h" /* MR_BOOK_MAX_PAGES(条漫排版页数上限) */
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
int mr_nav_tap_zone(int x, int view_w)
|
||||
{
|
||||
if (view_w <= 0) return MR_ZONE_MIDDLE;
|
||||
if (x < view_w / 3) return MR_ZONE_LEFT;
|
||||
if (x >= (view_w * 2) / 3) return MR_ZONE_RIGHT;
|
||||
return MR_ZONE_MIDDLE;
|
||||
}
|
||||
|
||||
int mr_nav_tap_delta(int rtl, int zone)
|
||||
{
|
||||
if (zone == MR_ZONE_MIDDLE) return 0;
|
||||
if (rtl) return (zone == MR_ZONE_LEFT) ? 1 : -1; /* RTL:左进右退 */
|
||||
return (zone == MR_ZONE_LEFT) ? -1 : 1; /* LTR:右进左退 */
|
||||
}
|
||||
|
||||
int mr_nav_swipe_delta(int rtl, int dx)
|
||||
{
|
||||
if (dx == 0) return 0;
|
||||
if (rtl) return dx > 0 ? 1 : -1;
|
||||
return dx > 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
int mr_nav_clamp_page(int page, int count)
|
||||
{
|
||||
if (count <= 0) return -1;
|
||||
if (page < 0) return 0;
|
||||
if (page >= count) return count - 1;
|
||||
return page;
|
||||
}
|
||||
|
||||
int mr_nav_commit_dist(int view_w)
|
||||
{
|
||||
int t = view_w / 4;
|
||||
if (t < 60) t = 60;
|
||||
if (t > 160) t = 160;
|
||||
return t;
|
||||
}
|
||||
|
||||
int mr_nav_commit(int dx, int view_w)
|
||||
{
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
return adx >= mr_nav_commit_dist(view_w);
|
||||
}
|
||||
|
||||
int mr_nav_fit_size(int fit, int src_w, int src_h, int vw, int vh,
|
||||
int *out_w, int *out_h)
|
||||
{
|
||||
if (!out_w || !out_h || src_w <= 0 || src_h <= 0 || vw <= 0 || vh <= 0) return -1;
|
||||
long long w, h;
|
||||
if (fit == MR_FIT_ORIGINAL) {
|
||||
w = src_w;
|
||||
h = src_h;
|
||||
} else if (fit == MR_FIT_HEIGHT) {
|
||||
h = vh;
|
||||
w = (long long)src_w * vh / src_h;
|
||||
} else {
|
||||
w = vw;
|
||||
h = (long long)src_h * vw / src_w;
|
||||
}
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
*out_w = (int)w;
|
||||
*out_h = (int)h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_nav_zoom_size(int w, int h, int zoom_pct, int *out_w, int *out_h)
|
||||
{
|
||||
if (!out_w || !out_h || w <= 0 || h <= 0 || zoom_pct <= 0) return -1;
|
||||
long long ww = (long long)w * zoom_pct / 100;
|
||||
long long hh = (long long)h * zoom_pct / 100;
|
||||
if (ww < 1) ww = 1;
|
||||
if (hh < 1) hh = 1;
|
||||
*out_w = (int)ww;
|
||||
*out_h = (int)hh;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_nav_pan_limit(int content_w, int content_h, int vw, int vh,
|
||||
int *lim_x, int *lim_y)
|
||||
{
|
||||
int lx = content_w - vw;
|
||||
int ly = content_h - vh;
|
||||
if (lx < 0) lx = 0;
|
||||
if (ly < 0) ly = 0;
|
||||
/* 平移量是「中心偏移」:可左右各移动溢出量的一半,两边才都看得到。 */
|
||||
if (lim_x) *lim_x = lx / 2;
|
||||
if (lim_y) *lim_y = ly / 2;
|
||||
}
|
||||
|
||||
void mr_nav_pan_clamp(int content_w, int content_h, int vw, int vh,
|
||||
int *pan_x, int *pan_y)
|
||||
{
|
||||
int lx, ly;
|
||||
mr_nav_pan_limit(content_w, content_h, vw, vh, &lx, &ly);
|
||||
if (pan_x) {
|
||||
if (*pan_x > lx) *pan_x = lx;
|
||||
if (*pan_x < -lx) *pan_x = -lx;
|
||||
}
|
||||
if (pan_y) {
|
||||
if (*pan_y > ly) *pan_y = ly;
|
||||
if (*pan_y < -ly) *pan_y = -ly;
|
||||
}
|
||||
}
|
||||
|
||||
int mr_nav_slide_from(int rtl, int delta, int view_w)
|
||||
{
|
||||
if (!delta) return 0;
|
||||
int from = rtl ? -delta : delta;
|
||||
return from * view_w;
|
||||
}
|
||||
|
||||
int mr_nav_fit_next(int fit)
|
||||
{
|
||||
if (fit < 0 || fit >= MR_FIT_COUNT) return MR_FIT_WIDTH;
|
||||
return (fit + 1) % MR_FIT_COUNT;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条漫排版 / 滚动窗口 */
|
||||
/* ============================================================ */
|
||||
static int strip_page_height(int src_w, int src_h, int vw)
|
||||
{
|
||||
long long h;
|
||||
if (vw <= 0) vw = 1;
|
||||
if (src_w > 0 && src_h > 0)
|
||||
h = (long long)src_h * vw / src_w; /* 适宽等比 */
|
||||
else
|
||||
h = (long long)vw * 3 / 2; /* 未知尺寸:2:3 竖图占位 */
|
||||
if (h < 1) h = 1;
|
||||
if (h > MR_STRIP_MAX_PAGE_H) h = MR_STRIP_MAX_PAGE_H;
|
||||
return (int)h;
|
||||
}
|
||||
|
||||
void mr_strip_free(mr_strip *s)
|
||||
{
|
||||
if (!s) return;
|
||||
free(s->top);
|
||||
free(s->height);
|
||||
s->top = NULL;
|
||||
s->height = NULL;
|
||||
s->count = s->total = 0;
|
||||
s->vw = s->vh = 0;
|
||||
}
|
||||
|
||||
int mr_strip_build(mr_strip *s, int count, int vw, int vh,
|
||||
mr_strip_meta_fn meta, void *ctx)
|
||||
{
|
||||
if (!s) return -1;
|
||||
mr_strip_free(s);
|
||||
if (count <= 0 || count > MR_BOOK_MAX_PAGES || vw <= 0 || vh <= 0) return -1;
|
||||
|
||||
s->top = (int *)calloc((size_t)count + 1, sizeof *s->top);
|
||||
s->height = (int *)calloc((size_t)count, sizeof *s->height);
|
||||
if (!s->top || !s->height) { mr_strip_free(s); return -1; }
|
||||
|
||||
s->count = count;
|
||||
s->vw = vw;
|
||||
s->vh = vh;
|
||||
long long y = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int sw = 0, sh = 0;
|
||||
if (meta && !meta(ctx, i, &sw, &sh)) { sw = sh = 0; } /* 未知/失败 → 占位 */
|
||||
int h = strip_page_height(sw, sh, vw);
|
||||
s->height[i] = h;
|
||||
s->top[i] = (int)y;
|
||||
y += h;
|
||||
if (y > 1000000000LL) { /* 累计偏移上限(防溢出;正常书远达不到) */
|
||||
y = 1000000000LL;
|
||||
for (int k = i + 1; k < count; k++) {
|
||||
s->height[k] = 1;
|
||||
s->top[k] = (int)y;
|
||||
y += 1;
|
||||
if (y > 1000000000LL) y = 1000000000LL;
|
||||
}
|
||||
s->top[count] = (int)y;
|
||||
s->total = (int)y;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
s->top[count] = (int)y;
|
||||
s->total = (int)y;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_strip_page_h(const mr_strip *s, int index)
|
||||
{
|
||||
if (!s || !s->height || index < 0 || index >= s->count) return 0;
|
||||
return s->height[index];
|
||||
}
|
||||
|
||||
int mr_strip_top(const mr_strip *s, int index)
|
||||
{
|
||||
if (!s || !s->top || index < 0 || index > s->count) return 0;
|
||||
return s->top[index];
|
||||
}
|
||||
|
||||
int mr_strip_total(const mr_strip *s)
|
||||
{
|
||||
return s ? s->total : 0;
|
||||
}
|
||||
|
||||
int mr_strip_max_offset(const mr_strip *s)
|
||||
{
|
||||
if (!s || s->count <= 0) return 0;
|
||||
int m = s->total - s->vh;
|
||||
return m > 0 ? m : 0;
|
||||
}
|
||||
|
||||
int mr_strip_page_at(const mr_strip *s, int off)
|
||||
{
|
||||
if (!s || s->count <= 0 || !s->top) return -1;
|
||||
if (off <= 0) return 0;
|
||||
if (off >= s->total) return s->count - 1;
|
||||
int lo = 0, hi = s->count - 1;
|
||||
while (lo < hi) { /* 最大的 top[i] <= off */
|
||||
int mid = lo + (hi - lo + 1) / 2;
|
||||
if (s->top[mid] <= off) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
int mr_strip_offset_for_page(const mr_strip *s, int page)
|
||||
{
|
||||
if (!s || s->count <= 0) return -1;
|
||||
if (page < 0) page = 0;
|
||||
if (page >= s->count) page = s->count - 1;
|
||||
int off = s->top[page];
|
||||
int m = mr_strip_max_offset(s);
|
||||
if (off > m) off = m;
|
||||
if (off < 0) off = 0;
|
||||
return off;
|
||||
}
|
||||
|
||||
int mr_strip_window(const mr_strip *s, int off, int max_pages,
|
||||
int *first, int *last)
|
||||
{
|
||||
if (first) *first = 0;
|
||||
if (last) *last = -1;
|
||||
if (!s || s->count <= 0) return 0;
|
||||
if (max_pages <= 0) max_pages = 1;
|
||||
|
||||
int m = mr_strip_max_offset(s);
|
||||
if (off < 0) off = 0;
|
||||
if (off > m) off = m;
|
||||
|
||||
int f = mr_strip_page_at(s, off);
|
||||
int l = mr_strip_page_at(s, off + s->vh - 1);
|
||||
if (l < f) l = f;
|
||||
|
||||
int lo = f - 1; /* 视口 ±1 页 */
|
||||
int hi = l + 1;
|
||||
if (lo < 0) lo = 0;
|
||||
if (hi > s->count - 1) hi = s->count - 1;
|
||||
if (hi - lo + 1 > max_pages) { /* 驻留上限:从视口中心向外截 */
|
||||
int center = (f + l) / 2;
|
||||
lo = center - (max_pages - 1) / 2;
|
||||
hi = lo + max_pages - 1;
|
||||
if (lo < 0) { lo = 0; hi = max_pages - 1; }
|
||||
if (hi > s->count - 1) { hi = s->count - 1; lo = hi - max_pages + 1; }
|
||||
if (lo < 0) lo = 0;
|
||||
}
|
||||
if (first) *first = lo;
|
||||
if (last) *last = hi;
|
||||
return hi - lo + 1;
|
||||
}
|
||||
|
||||
int mr_strip_at_end(const mr_strip *s, int off)
|
||||
{
|
||||
if (!s || s->count <= 0) return 0;
|
||||
return off >= mr_strip_max_offset(s);
|
||||
}
|
||||
|
||||
int mr_strip_at_start(const mr_strip *s, int off)
|
||||
{
|
||||
if (!s || s->count <= 0) return 0;
|
||||
return off <= 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 跳页状态 */
|
||||
/* ============================================================ */
|
||||
void mr_jump_cancel(mr_jump *j)
|
||||
{
|
||||
if (!j) return;
|
||||
j->pending = 0;
|
||||
j->target = -1;
|
||||
j->t0 = 0;
|
||||
}
|
||||
|
||||
void mr_jump_begin(mr_jump *j, int page, int count, unsigned long long now)
|
||||
{
|
||||
if (!j) return;
|
||||
if (count <= 0 || page < 0 || page >= count) { mr_jump_cancel(j); return; }
|
||||
j->pending = 1;
|
||||
j->target = page;
|
||||
j->t0 = now;
|
||||
}
|
||||
|
||||
int mr_jump_active(const mr_jump *j)
|
||||
{
|
||||
return j && j->pending ? 1 : 0;
|
||||
}
|
||||
|
||||
int mr_jump_target(const mr_jump *j)
|
||||
{
|
||||
return (j && j->pending) ? j->target : -1;
|
||||
}
|
||||
|
||||
int mr_jump_poll(mr_jump *j, int have_img, unsigned long long now,
|
||||
unsigned long long timeout_ms, int *timed_out)
|
||||
{
|
||||
if (timed_out) *timed_out = 0;
|
||||
if (!j || !j->pending) return 0;
|
||||
if (have_img) { mr_jump_cancel(j); return 1; }
|
||||
if (timeout_ms > 0 && now - j->t0 >= timeout_ms) {
|
||||
mr_jump_cancel(j);
|
||||
if (timed_out) *timed_out = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/* logic/reader/nav.h — 阅读导航纯逻辑(方向语义 / tap 分区 / fit / 平移 clamp)。
|
||||
*
|
||||
* 纯 C、无 SDK 依赖:reader.c 与宿主测试共用同一实现(logic/reader/nav.c)。
|
||||
*
|
||||
* 方向语义(mr_reader_get_direction,默认 RTL):
|
||||
* - LTR(左→右读):页码沿屏幕向右递增;屏左 1/3 = 上一页,屏右 1/3 = 下一页;
|
||||
* 手指向右拖露出左边那页 = 上一页。
|
||||
* - RTL(日漫,默认):页码沿屏幕向左递增;屏左 1/3 = **下一页**,屏右 1/3 = 上一页;
|
||||
* 手指向右拖露出左边那页 = 下一页。
|
||||
* 图像本身不做镜像(RTL 只影响翻页语义)。
|
||||
*/
|
||||
#ifndef MR_READER_NAV_H
|
||||
#define MR_READER_NAV_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- fit 三档 ---- */
|
||||
#define MR_FIT_WIDTH 0 /* 适宽:撑满屏宽 */
|
||||
#define MR_FIT_HEIGHT 1 /* 适高:撑满屏高 */
|
||||
#define MR_FIT_ORIGINAL 2 /* 原始:1:1 像素 */
|
||||
#define MR_FIT_COUNT 3
|
||||
|
||||
/* ---- tap 分区(按屏幕横向三分) ---- */
|
||||
#define MR_ZONE_LEFT 0
|
||||
#define MR_ZONE_MIDDLE 1
|
||||
#define MR_ZONE_RIGHT 2
|
||||
|
||||
/* 屏宽三分:左/中/右。view_w<=0 返回 MIDDLE。 */
|
||||
int mr_nav_tap_zone(int x, int view_w);
|
||||
|
||||
/* tap 分区的页增量:+1=下一页 / -1=上一页 / 0=chrome 显隐(中区)。 */
|
||||
int mr_nav_tap_delta(int rtl, int zone);
|
||||
|
||||
/* 中部横向拖动的页增量。dx>0 = 手指向右(露出屏幕左侧那页)。
|
||||
* 首/末页不在这里判断——调用方用 mr_nav_clamp_page 落到实际目标页。 */
|
||||
int mr_nav_swipe_delta(int rtl, int dx);
|
||||
|
||||
/* 页下标 clamp;count<=0 返回 -1。 */
|
||||
int mr_nav_clamp_page(int page, int count);
|
||||
|
||||
/* 翻页提交阈值:视口宽的 1/4,夹在 [60,160] px。 */
|
||||
int mr_nav_commit_dist(int view_w);
|
||||
|
||||
/* 松手位移是否达到翻页阈值。 */
|
||||
int mr_nav_commit(int dx, int view_w);
|
||||
|
||||
/* fit 目标尺寸(未含 zoom)。0=成功;参数非法 -1。 */
|
||||
int mr_nav_fit_size(int fit, int src_w, int src_h, int vw, int vh, int *out_w, int *out_h);
|
||||
|
||||
/* 按百分比放大(zoom_pct=100 原样;200=2×)。0=成功。 */
|
||||
int mr_nav_zoom_size(int w, int h, int zoom_pct, int *out_w, int *out_h);
|
||||
|
||||
/* 平移半幅:内容比视口每侧多出的像素(>=0)。内容不溢出该轴 → 0(该轴不可平移)。 */
|
||||
void mr_nav_pan_limit(int content_w, int content_h, int vw, int vh, int *lim_x, int *lim_y);
|
||||
|
||||
/* 把平移量夹到 ±半幅。 */
|
||||
void mr_nav_pan_clamp(int content_w, int content_h, int vw, int vh, int *pan_x, int *pan_y);
|
||||
|
||||
/* 翻页动画的进场起点(px):下一页在 RTL 从左侧进、LTR 从右侧进。delta=0 返回 0。 */
|
||||
int mr_nav_slide_from(int rtl, int delta, int view_w);
|
||||
|
||||
/* fit 档循环:适宽 → 适高 → 原始 → 适宽。 */
|
||||
int mr_nav_fit_next(int fit);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条漫(M7):竖向连续滚动的排版 / 滚动窗口 / 跳页状态 */
|
||||
/* ------------------------------------------------------------
|
||||
* 排版用页源契约的 meta()(原始像素尺寸,不解码):每页按屏宽等比换算显示高度,
|
||||
* 累加成「整条」的 y 偏移表。未知尺寸的页按 2:3 竖图占位(排版稳定,解码后不改布局)。
|
||||
* 全部纯整数运算、无 SDK 依赖;reader.c 与宿主测试共用(logic/reader/nav.c)。
|
||||
* ============================================================ */
|
||||
#define MR_STRIP_MAX_PAGE_H 200000 /* 单页显示高度上限(防坏头/极端长图撑爆累计偏移) */
|
||||
|
||||
typedef struct mr_strip {
|
||||
int count;
|
||||
int vw, vh;
|
||||
int *top; /* count+1 个:top[i]=第 i 页顶端偏移;top[count]=整条总高 */
|
||||
int *height; /* count 个:每页显示高度(>=1) */
|
||||
int total;
|
||||
} mr_strip;
|
||||
|
||||
/* 页尺寸回调(通常是 mr_book_meta):1=已知原始尺寸,0=未知。 */
|
||||
typedef int (*mr_strip_meta_fn)(void *ctx, int index, int *w, int *h);
|
||||
|
||||
/* 建排版表。0=成功;参数非法/内存不足 -1(已清空、可安全 free)。 */
|
||||
int mr_strip_build(mr_strip *s, int count, int vw, int vh,
|
||||
mr_strip_meta_fn meta, void *ctx);
|
||||
/* 释放偏移表并清零(可重复调用)。 */
|
||||
void mr_strip_free(mr_strip *s);
|
||||
|
||||
int mr_strip_page_h(const mr_strip *s, int index); /* 越界 0 */
|
||||
int mr_strip_top(const mr_strip *s, int index); /* index==count 返回总高;越界 0 */
|
||||
int mr_strip_total(const mr_strip *s);
|
||||
int mr_strip_max_offset(const mr_strip *s); /* max(0, total-vh) */
|
||||
/* 视口顶端(off)落在哪一页;空册 -1。 */
|
||||
int mr_strip_page_at(const mr_strip *s, int off);
|
||||
/* 落位到某页顶端所需的滚动量(夹到 [0,max_offset]);空册 -1。 */
|
||||
int mr_strip_offset_for_page(const mr_strip *s, int page);
|
||||
/* 视口可见页范围 [first,last] + ±1 解码窗,条数不超过 max_pages。
|
||||
* 返回窗口页数;空册 0。first/last 可 NULL。 */
|
||||
int mr_strip_window(const mr_strip *s, int off, int max_pages,
|
||||
int *first, int *last);
|
||||
int mr_strip_at_end(const mr_strip *s, int off); /* 1=已到条底 */
|
||||
int mr_strip_at_start(const mr_strip *s, int off); /* 1=已在条顶 */
|
||||
|
||||
/* ---- 跳页(chrome 滑块):目标页预解码完成后再落位,避免白屏/闪烁 ---- */
|
||||
typedef struct mr_jump {
|
||||
int pending; /* 1=正在等目标页 */
|
||||
int target; /* 目标页;pending==0 时无意义 */
|
||||
unsigned long long t0; /* 开始等待的时刻(ms) */
|
||||
} mr_jump;
|
||||
|
||||
/* 开始跳页:page 夹到 [0,count);count<=0 或 page<0 时取消。 */
|
||||
void mr_jump_begin(mr_jump *j, int page, int count, unsigned long long now);
|
||||
int mr_jump_active(const mr_jump *j);
|
||||
int mr_jump_target(const mr_jump *j); /* 无 pending 返回 -1 */
|
||||
void mr_jump_cancel(mr_jump *j);
|
||||
/* 轮询:have_img=目标页是否已解码。1=可以落位(已清除 pending);
|
||||
* timed_out 非 NULL 时写入「是否因超时落位」(调用方据此提示降级)。
|
||||
* 未就绪且未超时返回 0(继续等)。 */
|
||||
int mr_jump_poll(mr_jump *j, int have_img, unsigned long long now,
|
||||
unsigned long long timeout_ms, int *timed_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_READER_NAV_H */
|
||||
Loaded 100 of 142 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user