Compare commits

...
72 Commits
Author SHA1 Message Date
AskaEth 55a4a42488 Merge branch 'feat/in-folder-resume-button-in-library-top-b' 2026-11-06 19:35:46 +08:00
AskaEth c5985b2c67 图库: 进入文件夹后顶栏显示「续读」,一键回到上次那页(M33)
用户要求:文件夹拆开可进之后仍保留进度,但入口只在**进入了该文件夹**之后才出现,
不在上层列表的文件夹行加角标/按钮。

- ui/library/library.c:顶栏按 mr_cont_resume_page(g_cur_dir, 图片数) 判定,有进度才加
  MEOW_ICON_PLAY_ARROW「续读」动作(label 保留为语义名);点按 = mr_reader_open_dir_at
  (g_cur_dir, MR_READER_START_RESUME)。SDK 的 TopAppBar 动作创建期固定、无隐藏 API,
  因此可见性变化时整条顶栏重建:无进度是真的 2 个动作(没有空占位/死点击区),
  续读插最左,刷新/设置的下标与命中区不变。
- 可见性实时重判:load_dir(进入/返回/刷新)与 library_page_enter(从阅读器返回,
  进度可能刚写盘)都调 resume_refresh;根目录有进度时同样显示。
- logic/reader.c/.h:新增 mr_cont_resume_page(dir, pages)——无记录/空目录 → -1(不显示);
  保存页超页数(文件被删)→ 钳到末页并 INFO 日志;页数未知(列表被 2000 截断)不夹持,
  上界交给阅读器 open_book 自己的 mr_nav_clamp_page。
- 不改列表行语义(不重新引入 M18 删掉的「目录自动当书」)。
- 宿主回归(test_browse_ui_host U8):有进度目录 3 个动作/无进度 2 个、三个动作的点击区
  互不遮挡、点续读从当前目录续读页打开、超页数钳到末页、空目录不显示、上层/进入/返回
  与阅读器返回后的可见性切换;test_library T5 覆盖 mr_cont_resume_page 的全部边界。
2026-11-06 19:35:43 +08:00
AskaEth f2c489d89b Merge branch 'feat/tuku-page-slot-rgb565-residency' 2026-11-06 19:22:56 +08:00
AskaEth db92cbd95a 图库: 修 M32 评审 P2(预算注释按 3 槽口径,纯注释)
- reader.c 头注释与页缓存注释:真机 480×853 页「单页 1599KB → 800KB(−50%);
  3 槽常驻 3.2MB → 2.4MB」,不再是「两页 1.6MB」。
- MR_RESIDENT_CAP 注释:典型稳态 3 槽 = 2.4MB(窗口只落 2 页时 1.6MB),上限 3.2MB。
- 无判定/常量/代码改动(git diff 只有注释行)。
2026-11-06 19:17:29 +08:00
AskaEth f647a3c498 图库: 修 M32 评审 P1(plan_decode_long 仍按 4B/px)+ 补 480×853 回归(r2)
- P1:plan_decode_long 的判据从 l*s*4 改成 l*s*2(MR_PAGE_BYTES_CAP 已是 565 口径)。
  改前真机 480×853 页被压到 359×639(448KB)且每帧重采样;改后原尺寸落槽 799KB、
  绘制走原生 blit(scaled 增量 0)。
- 测试:新增 perf P13 —— 480×853 单页册,断言 slot_kb==480*853*2/1024、resident 相等、
  draw 后 blit 增长且 scaled 增量为 0;现有夹具(480×640/600×960)都在阈值以下,抓不到。
- P2:decode 日志 ok/fail 也打 peak+convert(与 skip 同口径);CBZ 500 页的「前缀字节」
  改成 mz_read_prefix 实测(不再用 500×4096 硬算),并说明 zlib 窗口不计入 --wrap 峰值;
  565 页图 alpha 忽略的行为差异写进 meow_ui_draw.h / gfx 注释;MR_ZOOM_TEMP_CAP 注释更正。
2026-11-06 19:01:16 +08:00
AskaEth 7036020005 图库: 阅读器页槽改 RGB565 常驻,页图内存减半(M32)
- 页槽从 RGBA 改成「已落框的 RGB565 位图」(2B/px):真机 480×853 页 1599KB → 800KB,
  两页常驻 3.2MB → 1.6MB。解码仍在 worker 里产出 RGBA,当场转 565 并释放 RGBA。
- SDK 新增 565 原语(meow_ui_draw.h / meow_ui_gfx.c):meow_gfx_blit_rgb565 命中页每行
  一次 memcpy(单次拷贝、无每帧重采样),缩放兜底走 565 最近邻。量化与 back_buf 用同一个
  rgb565(),565 屏上落下的像素与旧 RGBA 路径逐像素一致(不引入色偏/新增色带)。
- 预算按 565 重算:单页像素上限沿用旧 3MB@RGBA 的同几何口径(=1.5MB@565),
  MR_RESIDENT_CAP = 3 槽 + 1 张在途 = 3.2MB;reader_estimate_peak 新增 convert_bytes
  (落槽暂存 2B/px)与 peak_bytes 一起参与门控,避免实测超预算。
- 结果队列(已解码、未落槽)字节计入总预算(pending_bytes),不再「偷吃」最多 2 页。
- CBZ meta() 改读条目 4KB 前缀(mz_read_prefix;deflate 解到 4KB 即停,输入缓冲走栈),
  500 页排版不再触发 500 次全量解压。
- 测试:B3/B8 的 565 常驻与在途预算断言、CBZ 500 页 stored/deflate 前后对比、
  P12 565 与 RGBA 路径的逐像素颜色对比(量化误差/RMSE/灰阶梯)。
2026-11-06 18:43:11 +08:00
AskaEth bbc73635a6 Revert "Merge branch 'feat/reader-low-res-first-then-sharpen'"
This reverts commit eb0750051f, reversing
changes made to ce34974727.
2026-11-06 18:33:05 +08:00
AskaEth eb0750051f Merge branch 'feat/reader-low-res-first-then-sharpen' 2026-11-06 17:28:12 +08:00
AskaEth e896a28fba 图库: 修 M31 评审 P1(两档共存峰值 + 换清晰挤掉预取)与三个 P2(r2)
P1-1 内存账没算全:高档解码时低档还占着槽,但门控/预留只按新图算。
- 高档请求携带发起时的常驻快照(page_req.resident),解码前按
  「常驻 + reader_estimate_peak()」算这次操作的总峰值,上限 MR_TIER_TOTAL_CAP
  (12MB,与 page_source 的绝对上限同值;常态 3MB 只约束 stb 回退路径,
  后端页本来就超 3MB 且已被 M27 放行),超了跳过高档、保留低档(skip=coexist)。
- 峰值本身靠调度压回基线:分页模式焦点页还是低档时不预取邻居(预取来会被
  换清晰腾掉),换清晰前 slots_reserve 腾掉非可见页,高档只与焦点低档共存。
  B8(--wrap malloc,两实例并行):max_live_delta 3250KB→550KB,
  绝对峰值 5279KB→3932KB(基线 3929KB)。
P1-2 换清晰挤掉预取:换清晰后不重跑窗口,邻居洞留到下次翻页(0/8 命中、4 次/页)。
- 新增每页 in-flight 去重(g_req_inflight):请求被 worker 取走后不再被窗口刷新重排,
  这是重复解码/挤掉邻居的根因之一。
- 高档落位后按最近一次翻页方向补回邻居(sharpen_refill);反方向不补(3MB 常驻
  上限下放不下,必然再被挤掉)。
- 真机几何探针(480×720,1280×2275 JPEG,8 次 700ms/页):0/8→8/8 命中,
  4→1.5 次解码/页。B8 顺序 100 页:100/100 命中、149 次解码(1.49/页)。

P2-1:B8 改成 101 页册顺序前翻 100 次(每页都是不同的下一页),4MP/12MP 夹具
真正成为焦点页;断言最后一页=100、命中率 ≥90%、解码 ≤2/页+余量。
P2-2:B10 去掉恒真的 slot_kb 比较,改断言「总常驻不增加」+「高档操作增量 ≤3MB」+
「两档共存绝对峰值有界」;补双击放大后换清晰不重置缩放/平移、条漫滚动位置不变。
P2-3:B10 整串生命周期解码次数 ≤12(实测 7);B8 解码次数上限。
夹具用硬链接(同文件系统),避免多页册真拷贝几百 MB。

顺带:MR_SHARPEN_MS 200→150(任务允许 150–300ms,给「换清晰→补邻居」留翻页余量);
新增宿主观察口 flip_hits/misses、zoom/pan/woff(只读)。
2026-11-06 16:15:50 +08:00
AskaEth 62826e1d13 图库: 阅读器两级清晰度,先低档上屏、停稳再换清晰(M31)
真机 1280x2275 单页解码 392ms,首次看某页有一瞬加载感。改成:
- 大页(JPEG ≥1MP)先按显示长边一半的档位(真机页=step 4)解一版低档,
  精确落到显示框后立刻上屏(换图仍是单次原生 blit,不闪白、不重置缩放/滚动);
- 停稳 200ms 后才解目标档位(step 2),在**同一槽**原地替换低档,低档随即释放,
  常驻不翻倍;高档失败/预算跳过时保留低档,不把整页标成「内存不足」;
- 拖动/连翻/条漫滚动/跳页在途期间只解低档;高档请求复用 M22 的 gen/窗口
  stale 判据作废(只多一条 sharpen 谓词),出队前/后两条路径都计入取消;
- 日志尾部新增 tier=low|high(取消行同),真机可直接核对先糊后清。

分档只给 JPEG:step 是 JPEG 缩解概念,PNG/BMP 后端按目标逐行丢行本来就不慢,
且峰值模型对非 JPEG 按「输出=显示框」建模,多一条小图→落框路径会让模型低估
(不新增解码路径,也不改 sdk/ 与 page_source 的唯一模型)。

宿主回归:
- test_reader_membudget B10:JPEG 低档先落位(open 不阻塞)→ 高档落同槽
  (槽内字节不变、槽位全程不空)→ 拖动中强塞的高档请求被取消且不被解码;
- B8 连翻 100 页(6 类 1.3–12MP 循环)峰值增量 2714KB ≤3MB、常驻 ≤3MB,
  低档/高档都确实跑过;B3 断言小页保持单档(不付两次解码);
- test_reader_perf P11:非 JPEG 大页不分档,仍是一次原生 blit。
2026-11-06 13:10:01 +08:00
AskaEth ce34974727 Merge branch 'feat/pin-keypad-merge-ready-narrow-rework' 2026-11-06 11:58:16 +08:00
AskaEth ffc41bed9c 图库: 锁屏长按用例去掉恒真断言(M30 评审 P2-1)
「长按清空后仍停在 ACT_LOCK」这条断言测不出清空:进入该段前只输了 "135"
(3 位),而确认在 <4 位时本就 disabled,即使长按没清空也照样停在锁屏 →
断言恒真,与措辞不符。

删掉该断言与其前的空点确认;清空的真正判据仍是紧接着的「输 1357 → 确认 →
解锁」:若没清空,缓冲会是 "135"+"1357" = 7 位错码,必 FAIL。已用变异验证:
临时去掉 lock_clear_all() 后该行确实 FAIL,恢复后 PASS。
2026-11-06 11:50:22 +08:00
AskaEth 87f4ad0a82 图库: 锁屏长按退格在真机可达(仅在锁屏页启用长按)
M29 评审 P2-1:SDK 长按是 opt-in,全仓没有运行时调用
meow_ui_set_long_press_cb,meow_input_long_press_poll 恒返回 0,真机上
MEOW_EV_LONG_PRESS 永不产生——「长按退格清空」只表现为删一位。

- app.c 注册 no-op 长按回调,但只在 ACT_LOCK 为顶层页时启用(进主循环前对齐
  一次 + on_tick 每拍对齐),解锁 pop 后置 NULL。全程开启会让长按触发后的
  抬手不再产生 TAP/SWIPE,吞掉书库/阅读器里「按住 >=500ms」的点击。
- test_settings_lock_host:长按用例改走真实 SDK 路径——未注册 poll==0(P2
  根因)、注册后按住 700ms 产出 LONG_PRESS 且坐标=退格键心、被锁屏 view 消费、
  抬手不补发 TAP、解锁后关掉再 poll==0。
- lock.c 头注释补上该 opt-in 依赖,避免以后只看锁屏页时误判可达。
2026-11-06 11:46:31 +08:00
AskaEth 606a04a44e 图库: 锁屏 PIN 改页内自绘 12 键数字键盘(不再弹系统 IME)
真机 goodix 抬手长尾会让阻塞式 MEOW_IME_NUM 打开即被「点背景=取消」
关掉,表现为「PIN 按钮点好几下才弹键盘」。锁屏改自绘 3×4 键盘
(1-9 / 退格·0·确认,键 137×96、间距 10px、底边留 28px 上滑热区),
完全离线、无 IME 会话,首次点按必响应。

- 4-8 位(MR_PIN_MIN_LEN/MAX_LEN),顶部星号显示已输入位数,不回显明文;
  满 4 位后「确认」才可用;错误→提示+清空;通过→page_pop 回书库
- 长按退格清空;退格空缓冲为 no-op;8 位上限截断
- 颜色全部取 meow_theme_get();按键走 meow_btn_create_variant(TONAL/
  OUTLINED/FILLED),沿用 SDK 按压高亮/禁用变淡
- 锁屏仍消费全部 SWIPE/TAP/LONG_PRESS(左缘绕过 PIN 的路径不变),
  物理键返回 0 交 app.c on_key 仅日志
- 与设置页共用 logic/pin 的 mr_pin_verify/mr_pin_set,不复制校验逻辑
- 日志:lock: pad ready / pin wrong / pin ok(真机验收判据)

test_settings_lock_host.c:锁屏用例从 IME 注入改为按键点击(12 键归属、
键高/间距/热区几何、空确认/不足 4 位、错 PIN 清空重输、8 位上限+退格、
空退格、长按清空、不打开系统 IME);设置页仍走真实 IME 注入路径。
2026-11-06 11:46:31 +08:00
AskaEth 2af7095d35 Merge branch 'feat/wire-reader-gate-model-to-real-decoder-b' 2026-11-06 11:46:28 +08:00
AskaEth 11e09af7a4 图库: 修 M27 评审 P1 门控旁路 + 三个 P2(r1)
P1-1:判定为 backend 的页不再无条件放行 + 后端中途失败不再静默回退 stb。
- meow_img_load_file_step_ex 增加 allow_stb_fallback 参数:0 = 只试后端,
  失败返回 NULL(坏图占位),绝不回落 stb。阅读器按 dec.path 传入。
- 旧行为实测:截断 12MP JPEG 18,403kB、3000×3000 24bpp 头-only BMP 36,056kB、
  非法位深 PNG 16,409kB(stb 都"成功"出图)。修复后:
  截断 JPEG 275kB→NULL、头-only BMP 908kB→NULL、非法位深 PNG 门控占位(0 分配)。

P2-1:PNG 支持矩阵补 png_depth_ok 同式(color 2/4/6 仅 8/16;color 3 无 16;
color 0 允许 1/2/4/8/16),不再出现「矩阵说支持、后端拒绝」。
P2-2:后端命中页也保留两道安全阀:MemAvailable<6MB 占位、估算 >12MB 硬上限占位。
P2-3:B8 指标改为「窗口稳定后只 reset 一次」,量整个 30 翻窗口内相对稳态基线的
最大存活增量(淘汰不再掩盖峰值),并打印稳态/驻留/增量。
P2-4:page_source.h 的估算精度口径限定为「后端命中/典型页」。

回归:
- B7c 新增截断 12MP JPEG / 24bpp 头-only BMP / 非法位深 PNG,断言 backend-only
  NULL 且峰值 ≤3MB、或 stb 门控占位;B7 典型页与 B7b 回退矩阵保持全绿。
- test_reader_many_pages 恢复 24bpp 头-only 夹具(1bpp 保留):24bpp 判 failed、
  1bpp 判 skipped;断言 failed=2 / skipped=1。
- run_tests.sh / run_webtoon_tests.sh EXIT=0;sdk/test_meow_img_stream ALL PASS;
  交叉构建(SDK + tuku)零警告。
2026-11-06 11:46:16 +08:00
AskaEth 1725af631b 图库: 门控模型接真缩解后端 + _step 全格式后端(M27)
M23 交付了真缩解后端却「修好了用不上」:_step 只接 JPEG,而阅读器的峰值模型
仍是 stb 口径(PNG/BMP 按 W·H·4、JPEG 按缩解后整幅 RGBA),12MP/PNG 页全被判
超预算占位。本次把两处缝焊上:

- sdk/meow_ui_img.c:meow_img_load_file_step 的 JPEG-only 限制放开到 PNG/BMP;
  新增 meow_img_load_file_step_ex 回报实际路径(backend/stb),日志可核对预判。
- tuku/logic/reader/page_source.[ch]:新增 reader_estimate_peak(),成为峰值模型 +
  预算 + MemAvailable 门控的唯一实现(reader 不再另写一份)。
  * 后端命中(JPEG SOF0/1/2、≤3 分量、8bit、N=1..7;PNG 非交错/标准压缩/合法色型;
    BMP DIB≥40/BI_RGB/8·16·24·32bpp)→ 峰值 = 目标图 + 分量平面/行缓冲 + 固定开销,
    与源分辨率无关 → 直接放行,不门控;
  * 只有回退 stb(CMYK/算术编码、Adam7、RLE/1-4bpp、N≥8 小图等)才按 M22 stb 模型
    算峰值,超 3MB 或 MemAvailable <6MB 才占位。Adam7 另补 stb 的整幅暂存上界。
- reader:删掉本地 stb 模型/常量,统一调 reader_estimate_peak;日志加 path=backend|stb。
- 测试:6 类典型页(2000×3000 baseline/渐进/restart、1600×2400 PNG/BMP、4MP、12MP)
  断言全部 backend + 不占位 + 估算 ≥ 实测且 ≤1.5×(实测 1.03–1.08×);两条路径输出
  尺寸逐像素一致;CMYK/Adam7/1bpp 断言走 stb 并按旧门控占位;连翻 30 页无占位、
  单页分配峰值 ≤3MB;membudget 链接补 meow_ui_time.c 与 M23 两个解码 TU。
2026-11-06 11:46:16 +08:00
AskaEth 668629bda3 Merge branch 'feat/sdk-true-downscaling-decoders-for-jpeg-a' 2026-11-06 11:35:49 +08:00
AskaEth 6c8a1d3925 fix: 扫描结尾不再丢最后一个 MCU + 畸形 SOF 尺寸硬闸(M23 r2 P1-A/P1-B)
P1-A(像素错误):mj_boundary() 在「over 且 marker 非 RSTn」时 return 0 提前退出
扫描。熵码位缓冲最多 9 位前瞻,会在最后一个 MCU/块**之前**把扫描结束后的 marker
(DHT/EOI)吃掉;此时最后 1~2 个 MCU 的有效比特还在 bitbuf 里,return 0 把它们丢掉,
分量平面 memset(128) 的灰底直接露出来(低熵/纯白渐进式右下角 #808080)。
修:扫描循环本来就知道确定的 MCU/块数 —— 一律继续解完(位耗尽后 mj_eb 返回 0 做
零填充),marker 交给外层主循环。实测(纯白 2000x3000 progressive 无 restart):
修复前全图最小通道 128 / 与 stb 最大差 127 / >40 差值的通道 9 个;修复后
最小通道 253~255、最大差 ≤2、>40 通道 0。

测试加固(评审指出旧断言是抽样蒙混):
- S12「与无 restart 逐字节一致」改为**全图 memcmp**(去掉 64×64 网格 MAD<0.1 的兜底);
  修复后 9 档 restart 全部逐字节一致。
- 新增 S15:纯白 2000x3000 progressive(q50/60/70/80,无 restart)+ mostly-white q95,
  断言全图最小通道/右下角 24×24 最小通道 ≥250(丢 MCU 必露 128)、与 stb 全图逐像素比较。
  用 HEAD(11a4fcd) 的 meow_jpeg.c 重编同一测试:4 档纯白全部 FAIL(min=128,
  >40 通道 9),修复后全 PASS。

P1-B(内存放大/安全):mj_sof() 按 SOF 声明尺寸分配平面,降档循环在 base_n 已为 1
时不再生效,17~23 字节的 `SOI+SOF0(65535×65535)+EOI` 会让公开入口 _dim(720)
分配并 memset 64MiB(1 分量)/96MiB(3 分量)后才返回 NULL。
修:降档循环之后补硬闸 `if (plane_bytes + target_bytes > MJ_PEAK_CAP) return 0;`
→ 回退 stb(同文件 stb 是 0.02MiB 安全失败)。
回归 S16:构造 SOF 65535²(1/3 分量)→ 后端返回 0、公开入口 NULL、峰值 0.02MiB
(修复前 64.00 / 96.00MiB)。

meow_jpeg.h 注明「峰值只与输出尺寸相关」的适用范围:畸形/超大声明尺寸直接拒绝(安全失败)。

验证:test_meow_img_stream ALL PASS(含 ASan 整轮 rc=0 无报告);test_meow_img_budget /
test_meow_img_color ALL PASS;run_tests.sh rc=0(zip 106 / cbz 296 / membudget PASS);
run_webtoon_tests.sh rc=0;build_sdk.sh 零警告,MIPS32 rel2;体积 meow_jpeg.o 17,212B
+ meow_png_stream.o 8,456B。峰值不变(修复不增加分配)。
2026-11-06 11:21:03 +08:00
AskaEth 11a4fcd730 fix: 畸形 DHT 拒绝 + 渐进式 restart 重同步(M23 r1 P1-1/P1-2)
P1-1(内存安全):mj_huff_build 构造 9 位快表前校验规范码空间
(code + bits[l] > (1<<l) → 表非法),填表时再夹一道 <512 边界;mj_dht 失败
即返回 0 → 后端拒绝 → 回退 stb。此前过订阅 DHT(如「长度 2 的码 100 个」)会把
fast[512] 下标推到 12799,28~220 字节的畸形 JPEG 就栈溢出(ASan 实测
stack-buffer-overflow,非 ASan 直接 SIGSEGV/SIGBUS)。
回归:码长 1..4 × 符号数 3..255 共 1012 组畸形 DHT 全部后端拒绝、无崩溃;
「有效 JPEG + 注入坏 DHT」走拒绝 + 回退(不花屏)。

P1-2(能力):三处扫描循环的区间重同步统一到 mj_boundary,**只在 total == rst
(编码器区间长度)时重同步**。原实现一见「over && marker∈D0..D7」就重同步,但熵码
位缓冲有最多 9 位前瞻,over 会在区间真正结束前 1~2 个 MCU 置位——提前重同步会丢掉
bitbuf 里本区间剩余块的有效比特,随即失步(渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过
整个 MCU)。这正是 Pillow restart_marker_blocks=1/2/3/4/5/8/16/64/200 的渐进式
100% 回退 stb(25.8MiB)的根因。另修 mj_restart 裸扫把 FF 00(数据字节 0xFF 的填充)
里的 00 误当 marker。
回归:9 个渐进式 + restart 夹具全部 BACKEND=1、峰值 1.86MiB(与无 restart 相同)、
与同编码无 restart 文件逐字节一致(mad=0.000);baseline + restart 仍逐字节一致。

附带(评审 P2 两项,改动极小且已验证):
- PNG 8bit gray/RGB 的 tRNS 透明接通(8bit 采样值在 tRNS 低字节),与 stb 逐点 alpha 一致;
- 拒绝 16bit 量化表(只属 12bit 精度;收下会让 F*qt 在 mj_accim 里 int32 溢出,UB)。
- meow_img_load_file_step 的 JPEG 走真缩解后端(PNG/BMP 暂留 stb:阅读器预算模型仍是
  M22「整幅物化」口径,待 M27 同步后放开)。

验证:ASan 构建全 PASS;test_meow_img_stream 111 项全 PASS;test_meow_img_budget /
test_meow_img_color 全 PASS;tuku run_tests.sh rc=0(zip 106 / cbz 296,含 membudget)、
run_webtoon_tests.sh rc=0;build_sdk.sh 零警告,MIPS32 rel2 目标码。
峰值不变(修复不增加分配):2000x3000 baseline/progressive 1.86MiB、PNG 1.33MiB。
体积:meow_jpeg.o 17,284B + meow_png_stream.o 8,456B = 25.1KB。
2026-11-05 22:39:09 +08:00
AskaEth e9c0749609 test: 宿主链接行跟进 M23 解码 TU(meow_jpeg / meow_png_stream + -lz)
tuku/test/run_tests.sh 与 run_webtoon_tests.sh 里直接编译 meow_ui_img.c 的 6 条
gcc 行补上 "$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c;settings_lock_host 那条
原来没有 -lz(PNG 后端的 inflate 需要)。sdk/test_meow_img_color.c 头注释里的
编译命令同步(顺带补上 M21 起就缺的 meow_ui_time.c)。

只动链接行/注释,不改测试断言。
2026-11-05 21:33:54 +08:00
AskaEth d84dd949c7 SDK: 图片解码真缩解后端 —— JPEG DCT 域 / PNG 逐行 / BMP 跳行(M23)
stb 的 stbi_load_scaled 是「先整幅解、再重采样」:2000x3000 baseline 峰值 10.07MB、
渐进式 25.84MB、1600x2400 PNG 25.65MB,真机解码余量只有 ~4.2MB。新增解码后端在
解码期只算目标分辨率,峰值只与输出尺寸相关:

- meow_jpeg.c:自写纯 C JPEG 解码(baseline + progressive,Huffman)。按
  ceil(8*maxdim/长边) 选 N 点 DCT 缩解(N=1..7),基函数为「8 点 IDCT 按像素区间
  盒式平均」的 Q12 定点(DC 恒为 F00/8,N 之间亮度一致);系数边解边累加进分量平面,
  不留整幅系数阵列;渐进式跳过精细化扫描(Ah>0)、只解各频带首次扫描 —— 渐进式与
  baseline 内存足迹相同(这是 libjpeg 系列做不到的:它对渐进式必须整幅驻留 JBLOCK)。
  不支持项(4 分量 CMYK/YCCK、算术/无损/分层、12bit 精度)返回 0 回退 stb。
- meow_png_stream.c:libz 逐行 inflate + 反滤波,只把需要的列写进目标行;
  gray/gray+A/RGB/RGBA/调色板,1/2/4/8/16bit(16→8 取高字节);Adam7 等回退 stb。
  BMP 按目标行 seek + 行内跳列(8bpp 调色板/16bpp RGB555/24/32bpp),不读全图。
- meow_ui_img.c:load_file_dim / load_mem_dim 先进后端,失败/不支持回落 stb 旧路径
  (stbi_load_scaled + 精修);输出尺寸与 meow_img_downscale 同公式,对外语义不变。

宿主实测(--wrap malloc,maxdim=720,M23 前后同一夹具):
  2000x3000 baseline     10.07 → 1.86 MiB
  2000x3000 progressive  25.84 → 1.86 MiB
  1600x2400 PNG          25.65 → 1.33 MiB
  1600x2400 BMP          15.97 → 1.32 MiB
  4MP/8MP/12MP JPEG       9.67/13.37/20.12 → 2.30/2.20/2.29 MiB(不随分辨率线性增长)

sdk/test_meow_img_stream.c:峰值断言(≤2.5MiB)、与 stb 整幅+降采样的像素比对
(平均差 4.4-13.7/255,块均值结构相关 ≥0.88)、内存入口逐字节一致、回退矩阵
(CMYK/1bpp BMP/Adam7 PNG)、restart marker、缩解阶扫描 N=4/5/7、坏图容错。
build_sdk.sh 增加 meow_jpeg / meow_png_stream 两个 TU(MIPS32 -Os 零警告)。
体积:目标码 +24.9KB(meow_ui.a +25.5KB),picbrowser 静态二进制 +18.2KB。
2026-11-05 21:33:54 +08:00
AskaEth 88f3d861b0 Merge branch 'feat/tuku-reader-memory-peak-oom-fix' 2026-11-05 21:28:55 +08:00
AskaEth 40751c09bd Tuku reader: 真实解码峰值模型 + 3MB 硬阈值 + 解码取消 + 3 槽(M22 止损版)
真机(R3 Pro II 480×720 / MemTotal 56872kB):空闲 RSS 6044kB → 翻页 11400kB →
峰值 15600kB(MemFree 仅 1404kB)→ SIGKILL。M20 审计坐实根因是「单页解码瞬时峰值」,
且旧模型把 stb 当缩解库,对 JPEG 低估 3.5–8 倍(实测 baseline 2000×3000 +10.1MB、
渐进 +23.5MB、1200×1600 PNG +12.9MB)。本次按实测重定预算:

- 真实峰值模型 decode_peak_bytes():JPEG 分量面(+渐进 short 系数面)、PNG 源缓冲 +
  RGBA 转换副本、BMP 直写目标,再叠精修/落框两段一次性重采样;上限常态 3MB
  (MR_DECODE_PEAK_CAP),只有 MemFree 真富余时才放宽到 min(12MB, MemFree-4MB)。
- 超预算时的降级:JPEG 逐步加粗 stbi 缩解档位(meow_img_load_file_step),
  PNG/BMP 无解码期缩放 → 占位;结果精确重采样回显示框(meow_img_resample),
  命中页仍是单次 blit(M17 热路径纪律不破)。
- 占位可重试(g_page_skip 与永久失败 g_page_fail 分离):焦点页冷却 1.5s 自动重试、
  中间区点按立即重试、离开再回来即重试;坏图仍是永久占位。
- 解码前内存门控:MemAvailable < 6MB 不解码(读不到就放行,不猜)。
- 解码请求取消:worker 出队后、mr_book_open 前二次判 gen/窗口;拖动/fling 期间只解落点页,
  预取邻居仅当 MemAvailable ≥ 12MB。
- 先释放再解码:queue_page 按落地字节 slots_reserve() 先淘汰不可见旧图;可见页不淘汰。
- 槽位 4→3、常驻上限 9MB→3MB、结果队列 4→2。
- 可观测性:`reader decode page=N src=WxH peak=XkB memfree=YkB memavail=ZkB ok|skip=… step=S ms=M`
  与进出阅读器 rss/hwm/memfree/memavail。

page_source: 头部探测新增 codec/progressive/channels/planes(mr_probe_info),
只带最小 SOF 的合成头仍能取尺寸(旧契约不退化)。

宿主证据:新增 test_reader_membudget(--wrap malloc 计真实峰值):
PNG 600×960 model 4001kB vs 实测 3943kB;JPEG 1000×1500 step2 model 3749/实测 3711;
连翻 100 页 peak_transient 0kB(先释放再解码让缓冲复用)、驻留 ≤3MB、解码 100 次(取消生效)、
3MB 阈值下 1.5MP JPEG 可解、4MP JPEG 需 MemFree 放行(否则占位)、渐进式模型更高于 baseline。

run_tests.sh 全绿(含 zip 106 / cbz 325)、run_webtoon_tests.sh 全绿、SDK 与 tuku 交叉构建零警告。
2026-11-05 21:28:50 +08:00
AskaEth 773ab9ff92 Merge branch 'feat/sdk-mipsel-clock-abi-fix-sync' 2026-11-05 20:27:36 +08:00
AskaEth a07f9752d3 SDK: 内置 mipsel o32 时钟/睡眠 ABI 修复(meow_ui_time.c)+ 宿主单测
zig 0.17 内置 musl 在 mipsel 上头声明 64 位 time_t、实现走 time32 系统调用
(__ILP32__ 守卫),导致 meow_ui_time_ms() 毫秒位恒 0、高 32 位随机,
usleep(16000) 立即返回(帧节拍/轮询变忙等空转)。主仓已验证的修复逐字节
搬入公开仓:取时按内核 o32 ABI(8 字节 {int32 sec; int32 nsec})再升 64 位,
同 TU 强定义 __clock_gettime/64 与睡眠入口,链接期接管 libc。

- meow_ui_time.c / CLOCK_ABI.md:cp 搬运,md5 与主仓一致(含原注释与依据链)
- meow_ui_gfx.c:删掉 now_ms/meow_ui_time_ms(避免与 meow_ui_time.o 重复符号),
  6 处内部调用改外部 meow_ui_time_ms;<time.h> 不再使用
- meow_ui_internal.h:补 meow_time_ms_from_parts 声明
- build_sdk.sh:OBJS 首位加 meow_ui_time,头注释写明归档须排在 -lc 之前
- test_meow_time.c:适配公开仓构建方式,加负 sec 回绕两例,输出断言数(15)
- sdk/README.md:公开说明本次修复、链接顺序要求与已知限制(2038 前有效)
- tuku/test/run_tests.sh、run_webtoon_tests.sh:$SDK 源文件清单补 meow_ui_time.c
  (宿主集成测试直接编 SDK 源,否则链接期 undefined symbol)

验证:sdk/build_sdk.sh 0 warning;宿主 test_meow_time == PASS == checks=15;
tuku 两套测试全 PASS(run_tests 462+106+325 断言、webtoon 240 断言,0 failed);
探针/ picbrowser 反汇编确认 __clock_gettime 取自本 TU(8 字节 time32 + 升位),
无 libc VDSO 前导;picbrowser 仍为 MIPS32r2 静态 ELF。
2026-11-05 20:27:26 +08:00
AskaEth 4773ec1a61 Merge branch 'feat/tuku-library-file-manager-mode' 2026-11-05 20:27:19 +08:00
AskaEth 4befbfe14c 图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)
P1-1(会静默开错图):取消「书库从列表推导图片下标」。
- 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的
  全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确,
  再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。
- 删除 mr_library_image_index(不再有跨枚举的隐式契约)。
- 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。
- 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/
  library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411);
  评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**,
  2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。

连带处理(评审 r1):
1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth);
   顺手删掉 library_page_create 里同款死写。
2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted,
   book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册;
   根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、
   rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。
3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子
   mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。
4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。

回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例)
与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00
AskaEth 64336f95d9 图库: 书库改文件管理器模型 + 关闭缩略图 + 根锁定/无卡容错(M18)
用户反馈四件事一起改:
- 「目录里有一张图就被当成最终目录」→ 取消「目录含图=一本书」全部链路
  (探测/页数/封面/续读角标/懒算 worker/sr2 补位),列表 = 目录 + 图片 +
  .cbz/.zip(目录优先、组内自然序)。点目录=进入(空目录也进),点图片=
  mr_reader_open_dir_at(当前目录, 图片自然序下标),点档案册=打开档案册。
- 「文件夹浏览特别卡」→ 删除缩略图线程/磁盘 bmp 缓存/LRU/解码路径与行内
  缩略图;首帧只做一次 opendir/readdir+lstat,副标题(类型·大小)扫描期
  生成一次,行合成/滚动/点击零 IO 零格式化;去掉逐行 snap(自由滚动手感)。
  2000 项目录扫描 ~2.8ms(opendir=1, lstat=2000),行合成 2.78ms/200 万次
  (宿主实测;旧实现 2000 项 entry 含 512B cover,内存 2.7MB→1.7MB)。
- 「不要访问 SD 卡上级目录」→ 浏览根锁定 MR_ROOT_DIR:删除越根的顶栏
  「上级」/library_go_parent,根隐藏顶栏返回,左缘返回到根=退出 App。
- 「没有 SD 卡不要报错」→ 根不可访问(未插卡/挂载失败/根是文件)时友好空态
  「未检测到 SD 卡」+ 顶栏刷新/重进页面重试;子目录不可读才弹提示横幅。

集成陷阱:书库图片下标与阅读器目录册共享同一套过滤器(隐藏项/图片扩展名/
普通文件/符号链接)与 mr_natural_cmp,test_browse_lazy 直接对比
mr_dir_book_init 顺序,test_open_book_host 断言「点 img10 就开 img10」。

reader 侧 M17 遗留:mr_reader_open_dir_at 在阅读页已激活时先 pop 再 push,
走完整 leave/enter(SDK 对同 view 重复 push 是 no-op 且会压重复 activity)。

回归:run_tests.sh / run_webtoon_tests.sh 全 PASS;交叉构建零警告。
2026-11-05 20:26:40 +08:00
AskaEth d07b99056f Merge branch 'feat/sdk-scroll-wrap-slot-leak' 2026-11-05 19:53:11 +08:00
AskaEth c85877b447 SDK: 修 meow_scroll_wrap 池槽泄漏(free 归还 wrap 上下文槽)+ 64 次回收/交错/池满回归(M19)
meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽占一个,meow_scroll_free 只归还
会话槽、从不归还 wrap 槽也不清 ctx.w → 进程内最多成功 wrap 8 次,之后恒 NULL。图库
list_rebuild/load_dir 每次重建,进 8 个目录后列表丢惯性(g_scroll==NULL 走 scroll_by 兜底),
os.meow.files 同样每次 reload 重建。

- meow_scroll 结构内记 wrap_slot(创建置 -1),wrap 时写入槽下标;free 时按该下标清
  g_wrap_ctx[].w/lv 并归还 g_wrap_used,随后置 -1(二次 free 不触碰任何槽)。
  直接 create 的实例 wrap_slot=-1,free 不误清别人的槽。
- 池满语义不变:8 个同时活跃时第 9 个 wrap 仍返回 NULL(不越界);free(NULL) 安全。
- test_meow_ui_scroll 补回归:64 次 wrap/free 全可用(修复前 8/64,修复后 64/64)、free 后
  再 wrap、8 满 + 第 9 个 NULL + 释放其一即复用、两 wrap 交错 free 其一不串槽、直接 create
  不误清槽、list→listview→list 槽复用不残留 kind、free(NULL)/二次 free。
2026-11-05 19:38:55 +08:00
AskaEth 782a5f7c90 Merge branch 'feat/tuku-reader-perf-and-crash-fixes' 2026-11-05 18:49:23 +08:00
AskaEth ceb65dfd01 图库: 修条漫长页/竖翻每帧重采样(解码几何对齐排版)+ 判据与活锁(M17 评审 P1-1/P2)
评审用真实 UI 链路实测:600x960、800x1280(长宽比 1.6)在翻页拖动与条漫模式
都是 blit+0 / scaled+5;「先适高再切条漫」scaled+10。根因是解码几何来源与排版几何
不同源:decode_long_side() 把长边钳到屏长边 720(600x960 解成 450x720),而
strip_page_height() 按屏宽算出 480x768;mode_set() 又不动 g_fit,适高/原始进条漫
100% 重采样。

修复:
- 解码目标 = 排版真正要的显示框:新增 page_geometry_at()(条漫=strip 高度、宽=视口宽;
  翻页=fit/zoom),解码计划与绘页/就绪判据同源。
- plan_decode_long() 去掉屏长边钳制,只保留 MR_PAGE_BYTES_CAP(3MB) 硬上界,
  超长条漫按比例收缩(不再因为「高度 > 屏长边」就永远对不上)。
- slot 记录本次解码的 want 几何;slot_ready() 改为几何比较:
  fit/缩放/模式变化 → 重解一次(放大再缩回、适高→条漫回到纯 blit);
  被 3MB 钳住的解码 want 不变 → 不再反复重解(大屏活锁)。
- 解码几何随 page_req 下发、随 page_res 返回:worker 不再读 g_fit/g_zoom/g_vw/g_vh。
- 峰值估算把 downscale/缩解输出一并计入(P2-3)。
- 适宽长图(竖向单独溢出)不再算平移模式:1.6 比例页原先左右 tap/横拖会变成
  「水平平移 0px」的空操作,页面根本翻不动;现在仍按翻页处理(原始档/横向溢出不变)。
- 窗口不再主动清窗口外的页图:0→1→0 来回翻不再反复重解同一页(140 页测试
  decode 次数 21→14);容量仍由 MR_SLOTS + 字节淘汰兜底。
- state_reset 复位 g_meta/g_start_at(起始页在 on_enter 入口先取走),删重复注释。

测试:test_reader_perf 夹具改成 600x960(1.6)并补 800x1280、翻页拖动、条漫、
「适高→条漫」、双击放大缩回、静止无重解、大屏 1080x1920 无活锁;test_reader_many_pages
补 meow_ui_paint_full(绘制路径覆盖,P2-5)。
2026-11-05 18:28:52 +08:00
AskaEth 462cfd5bc0 图库: 阅读器翻页按显示尺寸解码(零重采样)+ 多页闪退防御 + 从第 N 张开始(M17)
用户反馈「横竖翻页都卡(40 页也卡)」「140 张图的文件夹一打开就闪退」
「要从中间某张开始看」三条,本次分别处理:

翻页卡顿(真因:每帧一次重采样)
- 解码目标从「屏长边」改成「当前 fit/zoom 的显示尺寸」:适宽 3:4 页原来解成
  540x720、显示 480x640,draw_page_at 于是每帧走 meow_ui_img_scaled —— 每页每帧
  多一遍全图最近邻重采样 + 一次 1.2MB 临时 malloc/free(>128KB 走 mmap)。
- 现在解码结果与显示框一致(±2px 容差)→ 翻页/滚动热路径只有原生 blit。
- fit/zoom 变化时按新几何重解(slot_ready 判据 + 同页替换),不再残留缩放;
  已到解码预算或已是原始分辨率的小图不会反复重解。
- 拖动/吸附/fling 帧只 invalidate 内容区(顶栏静态,不再每帧重绘标题文字);
  异步落位只在「该页真的在屏幕上」时才重绘,预取邻居不刷屏。
- 页原始尺寸按册缓存(8B/页),绘制几何不再每帧 fopen probe 文件。

多页闪退(宿主未能复现,按内存边界防御)
- 常驻页图改为「显示尺寸」并加上限:单页 ≤3MB、整窗 ≤9MB(超限淘汰最远页);
  常驻内存只与解码窗大小有关,不随页数增长。
- 解码中间峰值 >12MB 的页直接跳过并显示占位 + WARN(旧代码 PNG/BMP 放到 40MB,
  3000x3000 就是 36MB 瞬时分配;JPEG 完全没上限)。140 页夹具(含坏图/超大图)
  走真实 UI 链路打开、翻页、跳页、条漫全部通过。

新增 mr_reader_open_dir_at(dir, start_index)(文件管理器模式入口)
- 显式下标优先于续读记录;-1=续读(旧入口语义),其它负数钳到 0,≥页数钳到末页。
- mr_reader_open_dir 保留为 (dir, MR_READER_START_RESUME) 的包装;reader.h 导出。

测试:新增 test_reader_perf(翻页零重采样/fit 切换/驻留上界)与
test_reader_many_pages(140 页 + 坏图/超大图 + 起始下标/钳制/续读优先级 + 条漫跳页)。
2026-11-05 18:06:26 +08:00
AskaEth 73a9603ef0 Merge branch 'feat/tuku-one-click-pack-with-signing' 2026-11-05 17:40:57 +08:00
AskaEth 94eeabd789 图库: 打包改根布局并接入 meow_sign 一键签名(M16) 2026-11-05 17:37:50 +08:00
AskaEth e6a9e53ae9 Merge branch 'feat/tuku-open-book-and-back-gesture-fixes' 2026-11-05 13:07:51 +08:00
AskaEth 3cbcaaaac1 图库: 修系统返回语义与打开册资源路径(M15)
- 返回手势:书库列表排除左缘 24px 系统返回带并清空残留拖拽态;library_go_up
  到 TF 根返回 0(app.c 退出 App),不再上溯系统目录;顶栏返回改为显式
  library_go_parent(访问放开,可越过 TF 根到 /)。lock 页继续自行消费返回。
- 打开册:缩略图源图改用预算解码(meow_img_load_file_dim,峰值数十 MB→~1MB);
  离开书库页停止后台探测/缩略图线程,避免与 open_book 争内存/FD;打开失败按
  errno 记日志,ENOMEM/EMFILE/ENFILE/EAGAIN/EINTR 自动重试一次;修 open_book
  自拷贝 snprintf 的未定义行为。
- 测试:test_browse_ui_host 增加 TF 根退出/顶栏上级/左缘不拖拽断言;新增
  test_open_book_host(封面图+含图子目录的父目录 → reader 打开的完整 UI 链路)。
2026-11-05 13:03:20 +08:00
AskaEth 5ac227b225 Merge branch 'feat/tuku-fast-library-browsing' 2026-11-05 12:34:20 +08:00
AskaEth 66e598cb62 图库: 恢复缩略图 LRU 跳过解压页缓存(M11 P2-1 回归),补启动空列表/prune 回归测试(M13 评审 P2)
- ui/library/library.c cache_prune:rebase 时丢失了 M11 c775f85 的必修保护,
  恢复 `if (mr_cache_is_page_name(de->d_name)) continue;`(在 .bmp 过滤之前)。
  解压页缓存与缩略图同目录,mrpage_* 归 page_cache 自己的 64 页/32MiB LRU 管理;
  否则档案页被缩略图 LRU 统计并删除 → 每翻页重解压,且「touch 命中→解码」窗口内
  可能被删导致解码失败。
- 回归测试(MR_LIBRARY_UI_TEST_HOOKS 接缝):test_browse_ui_host 新增 U6——
  预置 1 个最旧 mrpage_*.bmp + 65 个较新缩略图,调用 library_test_cache_prune(),
  断言 mrpage_* 保留且不计入 64 张额度、缩略图被裁到 64;已验证去掉修复时该用例
  会失败(防止再次在 rebase 中丢失)。
- 顺手补齐/校正:
  ① test_browse_ui_host 的启动路径改为 create 前就把根指向不存在的目录,真实覆盖
     「启动根不可读仍建空列表」(断言 has_list/rows==0/横幅/当前目录),不再只是声明;
  ② tuku_internal.h 的 library_at_root 注释改为「书库根(生产=MR_ROOT_DIR,测试可改根)」。
- 复验:run_tests.sh 全 PASS(73/69/90/18/108/19/85 + settings 集成 + browse UI 集成
  + zip 106 + cbz 325);run_webtoon_tests.sh 全 PASS(76/36/71/57 + 集成);
  sdk/build_sdk.sh + build_tuku.sh 零警告 MIPS32r2 静态 ELF;UI host 测试 ASan/UBSan 全绿。
2026-11-05 12:31:46 +08:00
AskaEth 9f34417c49 图库: 单层浏览 + 懒判定与 M7/M11 整合;探测缓存 P2 修复(M13)
rebase 到 picbrowser(含 M7 条漫/续读、M11 CBZ 档案册)后的语义拼接:
- 保留单层扫描重写:一次 opendir/readdir+lstat 只列当前层(目录 + 注入 archive_pages
  时的 .cbz/.zip 档案文件),自然升序、隐藏项/符号链接跳过、2000 条截断标注;
  系统目录/空目录/无图目录一律列出;彻底删除递归「子树含图」探测
- 保留并复用 M11 档案册分支:add_dir_entry / add_archive_entry / entry_subtitle /
  entry_badge / archive_pages_at / sibling_is_book / mr_library_scan_ex 注入;
  档案册行同样先成行、页数由后台 probe(注入 zip 页数回调)懒算
- 保留 M7:mr_library_dir_has_images / mr_book_adjacent(_ex) 语义不变;为 adjacency
  恢复 ignored_dir_name(仅册间衔接跳过系统/隐藏目录,浏览本身不隐藏);
  mr_library_format_subtitle 在补位后拼「· 续读 Pn」角标
- mr_library_probe_path 统一探测目录/档案册:mtime(秒+纳秒)+size 失效的 256 项 LRU +
  互斥锁;评审 P2:只缓存「探测前后 stat 完全一致」的结果,期间变化则不写缓存
  (消除「不完整结果绑定新 mtime」的假新鲜;下次调用重新探测)
- UI:列表用 src2 每帧取行;后台线程探测优先于缩略图;档案册封面解压档内首图(M11);
  行语义=书页数/缩略图、空目录「空目录」、无图/坏档「无图片」;可上溯至文件系统 /、
  不可读目录提示并停留;TopAppBar「TF卡」回根;启动时根目录不可读也建空列表;
  顶栏回根命中加 g_tb_home_idx>=0 防御
- tuku_internal.h:library_go_up 注释校正(0=到达文件系统 /,app.c 据此退出)
- 测试:test_library 改单层语义;新增 browse_nav / browse_lazy / browse_perf /
  browse_ui_host(含评审 P2 的「探测期间变化不写缓存」用例);test_cont_adjacent A6
  改两段式;test_cbz T8/T10 改两段式(坏档/无图档案列出但非书);
  run_tests.sh 保留双方全部条目(browse 4 套 + run_zip_tests/run_cbz_tests)
- 复验:run_tests.sh 全 PASS(含 M11 zip/cbz)、run_webtoon_tests.sh 全 PASS;
  worktree 内 sdk/build_sdk.sh + build_tuku.sh 零警告静态 MIPS32r2 ELF
2026-11-05 12:23:31 +08:00
AskaEth c9cff99af3 Merge branch 'feat/tuku-cbz-integration' 2026-11-05 12:16:59 +08:00
AskaEth c775f8558c fix(tuku): 评审 P2-1/P2-2——解压页与缩略图缓存互删自愈 + 大页溢出文档化
P2-1(必修):解压页缓存与缩略图 LRU 在 *.bmp 上相互踩踏
- ui/library 的 cache_prune 先按 page_cache 的共用判据 mr_cache_is_page_name
  跳过 mrpage_*,不再把 BMP 解压页算进缩略图额度或 unlink(含 Vol10.cbz 这类
  BMP 档不再出现「被缩略图 LRU 删掉 → 首页解码失败」);
- mr_cache_touch 命中时 stat 校验文件存在与大小,缺失/尺寸不符即移出索引并按
  未命中返回,调用方重新解压——外部清理/掉电残留/误删都能自愈,不再返回死路径。

P2-2(文档化一次性溢出):
- page_cache.h 与 make_room_locked 写明:单条目本身超过 32MiB 时只保留该条、
  其它全部淘汰,占用上限受页源单页 64MiB(MR_ZIP_PAGE_MAX_BYTES)约束;
  大页仍可读,不因预算拒绝落缓存。
- 新增 33MiB 页夹具(PNG 头 + 可压零填充,deflate 后仅数十 KB)与断言:
  大页可读、cache_bytes 允许超 32MiB、小页被淘汰、再来小页时大页按预算淘汰。

顺手:page_cache.h 注释包名 os.meow.picobrowser → os.meow.picbrowser(测试路径同步)。

测试:test_cbz 267 → 296 项全 PASS;run_tests.sh(zip 106)与
run_webtoon_tests.sh 全绿;交叉构建 sdk/tuku 零警告,产物静态 MIPS32r2 ELF。
2026-11-05 12:13:47 +08:00
AskaEth 562cc44931 feat(tuku): CBZ 档案册集成——档案页源/书库列册/解压缓存
- logic/reader/archive_source.c:档案册页源实现 M6 mr_book 契约
  (mz_open/mz_count/mz_entry_at/mz_read_alloc),条目自然序 + 图片后缀 +
  路径安全(`..`/绝对路径/反斜杠/隐藏段/内嵌 NUL)+ 单页尺寸上限过滤;
  meta 按需解压只读图片头(不落缓存,结果按页记忆);mr_book_open_auto
  统一目录册/档案册开册,翻页与条漫两套消费代码零改动。
- logic/reader/page_cache.c/h:解压页落 MeowData/<pkg>/cache,LRU 64 页或
  32MiB,启动清 mrpage_* 孤儿,写入 .part + rename;与缩略图缓存同目录、
  前缀隔离;互斥量保护,解码/缩略图线程可并发。
- logic/library.c/h:扫描直接子文件里的 .cbz/.zip 成册(行=文件名+页数,
  点入直接阅读);档案探测由调用方注入(logic 不依赖 zip 容器),不注入时
  与 M7 行为逐字一致;册间衔接兄弟表=含图目录∪含页档案册,同列自然序。
  条目构造/副标题/角标/兄弟判据拆成小函数,便于 M13 书库重写拼接。
- ui/library:档案册行 + 封面缩略图(解压档内首图)、扫描注入探测、缓存
  目录统一由 page_cache 推导(一份规则)。
- ui/reader:open_book 走 mr_book_open_auto;跨册衔接纳入档案册。
- build_tuku.sh 补 zip/archive/cache 源与 sysroot zlib 头(zip.c include
  <zlib.h>);run_tests.sh 纳入 zip+cbz 用例;run_webtoon_tests.sh 的宿主
  集成构建补新源与 -lz;.gitignore 同步测试产物。
- test/test_cbz.c(267 项断言)+ make_cbz_samples.py 真实 cbz 夹具:
  列档/自然序/解压字节 CRC/路径安全/坏档与 zip 炸弹容错/LRU 与 32MiB 上限/
  启动清孤儿/续读角标/封面首图。
2026-11-05 11:58:12 +08:00
AskaEth 19620a7957 Merge branch 'feat/tuku-zip-reader-core' 2026-11-05 11:39:36 +08:00
AskaEth fbd4db3aec feat(tuku): zip 容器读取器核心(纯 C + zlib,central directory 校验/单条目按需解压) 2026-11-05 11:39:34 +08:00
AskaEth 6caf2bc6e6 Merge branch 'feat/tuku-webtoon-mode-and-continuity' 2026-11-05 11:39:24 +08:00
AskaEth ff97f3cee7 feat(tuku): M7 条漫模式与续读——模式一键切换/每册记忆、视口±1 懒解码、滑块跳页、册间衔接
- 条漫:meow_scroll 跟手+fling,按滚动位置懒解码视口±1 页(驻留上限 4 页);
  排版用 mr_book_meta 累加适宽高度(nav.c 纯逻辑,宿主可测)
- 模式:阅读 chrome 图标一键切翻页/条漫;每册模式与 last page 经 meow_config 持久化
  (reader.mode/page.<FNV1a64(册路径)>),重进该册恢复模式与页码
- 续读:离开/翻页/滚动停止节流落盘;书库扫描给含图册副标题追加「· 续读 Pn」
- 册间衔接:末页前进→同父目录下一含图册首页,首页后退→上一册末页;无相邻册轻提示不退出
- 跳页:chrome 内 meow_slider,目标页预解码完成后落位(2.5s 超时降级提示)
- 解码:改走 M12 预算解码 meow_img_load_file_dim;超大 PNG/BMP 先 probe 预判,
  超 40MiB 原始 RGBA 跳过并显示「图片无法显示」,12MiB 以上记 WARN(如实保留残余风险)
- 测试:新增条漫排版/跳页状态/模式续读持久化/册间边界宿主测试,以及真实 SDK 源码树上的
  阅读器集成测试(模式切换/跳页落位/续读/跨册),test/run_webtoon_tests.sh 一键运行
2026-11-04 21:49:33 +08:00
AskaEth 8cbb240931 Merge branch 'feat/mangareader-settings-and-pin-lock' 2026-11-04 21:32:17 +08:00
AskaEth cca3906c80 图库: M5 设置页(阅读方向/PIN/主题状态)与启动 PIN 锁屏;阅读方向持久化(评审 P1)
logic/pin(纯 C):加盐迭代哈希(FNV-1a ×1000 轮 + 每次设置新随机盐,16 位 hex)经
meow_config_* 持久化 pin.enabled/pin.hash/pin.salt;提供 set/verify/change/disable 与
4-8 位纯数字校验;enabled=1 但盐/哈希残缺时 fail-closed;落盘半途失败会还原旧盐,不留
「新盐+旧哈希」的双失效状态。

ui/settings(ACT_SETTINGS):MD3 分组表单(meow_listview 容器圆角卡 + 段头标签)——
阅读方向 RTL/LTR 走 mr_reader_set_direction;启动 PIN 开关与修改(开启/修改均两遍
MEOW_IME_NUM 确认);版本与主题状态行(跟随系统 + 当前明暗,随 app.c 主题 listener
整帧重绘)。控件一律在 on_enter 建(栈顶=ACT_SETTINGS),避免 §6.1 owner 绑到
ACT_LIBRARY 的跨页命中泄漏。

ui/lock(ACT_LOCK):lock_view_should_show() 读 logic/pin;启用时启动先入锁屏,验证前
不绘制任何书库内容;「输入 PIN」按钮弹系统数字键盘(MEOW_IME_NUM),错 PIN 就地提示
重试(无惩罚性锁定),正确则 page_pop 回书库;on_event 消费 SWIPE——左缘返回不能绕过
PIN 或退出 App(meow_view_dispatch_event 先于 app.c 的 on_swipe)。

logic/reader(评审 P1 修复,最小持久化):mr_reader_set_direction 成功切换后 best-effort
写 meow_config("reader.dir","rtl"/"ltr"),写失败只打 WARN 不阻断本次切换;
mr_reader_get_direction 首次调用惰性读一次配置恢复(无配置/非法值 → 默认 RTL),之后
纯内存快路径。宿主测试经 -DMR_READER_TEST_HOOKS 的 mr_reader_test_reset() 模拟重启。

测试/构建:test_pin(85 项,PIN 全分支,含单键半途落盘失败);test_settings_reader_persist
(19 项:默认/落盘/重启恢复/坏值/非法值/只读一次/写失败只告警);可选宿主集成
test_settings_lock_host(真实 SDK view/activity/widget + IME 注入,端到端验证错/对 PIN
解锁与设置页两遍开启;SDK 无源码时 SKIP);run_tests.sh 保持 46/46 并纳入上述测试
(reader.c 的配置/日志依赖由 test/settings_reader_stub.c 存根),build_tuku.sh 加入
logic/pin.c。交叉构建静态 MIPS ELF(zig-mipsel)零警告。

安全口径:PIN 防随手翻看,不是安全边界——TF 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
2026-11-04 21:25:17 +08:00
AskaEth 47325f2b81 Merge branch 'feat/sdk-image-budget-decode' 2026-11-04 21:19:03 +08:00
AskaEth 21bffa1aeb sdk: 图片层追加预算解码入口,JPEG 解码期缩解(峰值 43.7→14.0 MiB)
M6 finding(vuln/high):meow_img_load_file 必先整幅 RGBA 解码,1600×2400 漫画页
瞬时峰值 20.2 MiB(15.4 MiB 输出 + 5.5 MiB YCbCr 平面),超设备 ~14MB 口径,
App 侧无法规避。

调研(vendored stb v2.30):无解码期缩放能力——JPEG 走固定 hs/vs 重采样到全尺寸,
PNG 一次性 inflate 整幅 raw_image,公共 API 只有 load/load_16/loadf/info。
完整缩小 IDCT 需改编 1/2、1/4、1/8 IDCT + 进升级系数存储 + MCU 尺寸,改动面大且
易出静默像素错误,故不重写解码器。

改为受控的最小追加(sdk/stb_image.h):
- 新增 stbi_load_scaled / stbi_load_scaled_from_memory:JPEG 输出级按步长 s 抽取
  行/列(resample 状态机逐行推进,仅采样行做颜色转换),整幅原尺寸输出缓冲从不
  物化;scale<=1 与旧 API 逐字节等价,非 JPEG 忽略。
- 进升级 JPEG:各分量系数 IDCT 进像素平面后立即释放(原来拖到输出分配之后)。
- 旧公共 API 行为不变。

SDK 追加式入口(旧 API 一字未改):
- meow_img_load_file_dim / meow_img_load_mem_dim(path/buf, maxdim):stbi_info 头探测
  选 1/2、1/4、1/8 整数档(取「长边/s ≥ maxdim」最大档,不欠采样),再交既有
  meow_img_downscale 精修;小图/maxdim<=0 走旧路径。
- meow_img_probe_file:只读头取尺寸,供 App 在 OOM 前提示。

实测(宿主 test_meow_img_budget,--wrap malloc 打点,全部 PASS):
  8.32MP JPEG   43.70 → 13.96 MiB(新/旧 0.32)
  1600×2400 页  20.16 →  9.18 MiB
  progressive   43.70 → 35.83 MiB(系数阵列扫描期驻留,残余上限)
  PNG 2400×1600 25.65 → 25.65 MiB(stb 无 PNG 缩解,如实记录)
  BMP 2400×1600 14.65 → 15.23 MiB(同上,多一次降采样小缓冲)
缩解结果与「整幅解码+downscale」网格平均差 6.28;对 PIL 逐像素比对与全幅解码
同误差量级(mean ≤0.02, max ≤3)。

验证:build_sdk.sh 重编 meow_ui.a 0 warning;examples/hello 重链 0 warning;
tuku 用 MEOW_SDK_DIR 指向本分支 sdk 重链 0 warning;既有 test_meow_img_color 仍全 PASS。
2026-11-04 21:02:39 +08:00
AskaEth 5f40d6fd87 Merge branch 'feat/tuku-fluid-viewer-core' 2026-11-04 20:58:13 +08:00
AskaEth 23c9db73df feat(tuku): 阅读器左右翻页核心——页源契约/预解码窗口/跟手翻页/方向感知 tap
- 页源契约(logic/reader):count/meta/open/close + 目录册(文件夹即书)为第一个实现;
  meta 由纯 C 图片头探测(PNG IHDR/JPEG SOF/BMP)提供原始尺寸,条漫可先排版再取图,
  zip 册(M9/M11)只需另写 mr_book_ops,两种消费方式零改动复用
- 翻页引擎:pthread 后台解码窗口=当前±1 页(目录册 downscale 到屏幕长边),翻页只 blit
  缓存页图 + meow_anim 150ms 滑动吸附;未命中立即切页显示加载态、不在翻页路径解码;
  只 flush 内容区;decode/flip 首帧/翻页完成/进出内存全部 meow_log 打点
- 阅读页 UI:fit 三档(适宽/适高/原始,TopAppBar 动作循环,标题显示页码 x/y)+
  双击以点击点为锚放大(临时缓冲超上限自动降倍率)+ 单指拖拽/滑动步进平移;
  tap 三分区方向感知(RTL 默认:左=下一页,LTR 反之),中区切 chrome;
  左缘返回交给 app.c、屏底热区交给 SDK 回桌面,两者都不拦截
- 跟手:app.c 仅补 2 处极小路由(ACT_READER 的 down/move),reader 自行算位移;
  屏底 28px/左缘 24px 不进拖动,竖向主导放弃横滑
- 内存纪律:驻留 ≤3 张 downscale 页图;放大时窗口收缩到当前页;on_leave 停解码线程、
  释放全部页图/册索引/TopAppBar,并记录 VmRSS/MemAvailable 前后对比
- 测试/构建:新增 test_reader_core(契约边界/空册/越界/重复 open-close/首末页/头探测/
  方向语义/fit/平移夹取,108 项);run_tests.sh 与 build_tuku.sh 源列表接线;
  宿主 46+108 全 PASS,交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
2026-11-04 20:58:05 +08:00
AskaEth 28b67a7b8d Merge branch 'feat/sdk-host-test-fixes-d1-d7' 2026-11-04 20:26:48 +08:00
AskaEth 432875d4e6 sdk: 宿主测试头注释按实测修订 + select T1 探针修正
- test_meow_ui_select: T1 选中高亮探针移到容器卡内(x=12),
  适配 M6.37 起的 listview 默认容器模式;6/6 PASS
- 各 test_*.c 头注释补 -Iinclude、meow_ui_nav.c 及精简源列表最小补集
  (theme/widgets/notif_gesture/overlay/suppress/sdk_fixes/pinyin/ime_fix)
- test_sb_statusbar 头注释给出可链接命令(--allow-multiple-definition)
- notify_post/ui_audio_host/p1p2p3 标注需主仓组件,公开仓跳过
- docs/APP_DEV_GUIDE.md: 去掉叠字与过强表述,改为 34/38 实测口径
2026-11-04 20:26:35 +08:00
AskaEth 2c60de141f 图库: 包名定为 os.meow.picbrowser(用户指定系统前缀)——app.json package/exec、build/pack 产物名与 README/.gitignore 同步;交叉构建+打包复验通过 2026-11-04 20:22:44 +08:00
AskaEth 429535e4d9 Merge branch 'feat/mangareader-skeleton-and-library' 2026-11-04 20:20:34 +08:00
AskaEth f4c21c74a1 fix(tuku): 书库显式启用 MD3 容器模式并补 M4/M5 接缝注释
- P1:list_rebuild() 在 set_item_renderer/set_item_tier 后调用 meow_listview_set_container(g_lv,1),
  修正 SDK 默认 container=0(扁平满幅+分隔线)与任务要求的 MD3 圆角卡观感不一致
- P2:reader/lock/settings 桩头补接缝注释——on_create 在 meow_view_create 内运行且 activity 栈空,
  非根页控件须 lazy 到 on_enter 或 push→建→pop;lock 另注明必须由自身 view on_event 消费 SWIPE,
  否则左缘滑动绕过 PIN
- 复验:宿主测试 46/46 PASS;交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
2026-11-04 20:09:24 +08:00
AskaEth bed6a95485 feat(tuku): 图库 M3 骨架——书库浏览/主题同步/阅读器与锁屏接缝
- 新增顶层 tuku/ 工程(package dev.tuku.app、二进制 tuku):app.c/internal 头/logic/ui 分层、app.json、build/pack 脚本
- 书库:从 TF 卡根浏览文件夹,含图目录=一本书;logic/library 纯 C 扫描+自然排序(page2<page10)
- 流式缩略图:后台线程解码+meow_img_downscale(96),首帧/滚动不阻塞;磁盘缓存 MeowData/<pkg>/cache(mtime 失效+LRU)
- 列表 meow_listview 虚拟化 + meow_scroll fling/snap;行=缩略图+目录名+页数
- 主题:全部语义色 + meow_theme_add_listener 即时重绘;明暗/风格族两态可读
- 导航:meow_view page push/pop、TopAppBar ⚙→设置;reader/settings/lock 最小桩
- 接缝:mr_reader_get/set_direction(默认 RTL)、lock_view_should_show()(桩 0)
- 宿主 gcc 测试 46 项 PASS;交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
2026-11-04 19:44:18 +08:00
AskaEth c0396e40ce sync(SDK): 增量同步主仓 M6.37–M6.39 至公开 SDK 仓
sdk/:
- 同步 meow_ui_widget/input/gfx/lazygrid/audio/media/img 等源码与头
  (SDK 默认列表/容器控件默认 MD3 — meow_list_set_container 可回退;
   全局 App 内底部上滑回桌面 — meow_ui_input_is_home_swipe 系统保留手势;
   LazyGrid/LazyColumn/LazyRow MD3 化;叠加让位自抑改 app_id 判定;
   DSD 音频格式等)
- 新增独立模块源码 meow_img_color / meow_lyric / meow_lyric_view 及对应宿主 test_*.c
- 保留公开仓自包含适配:vendored meow_font.c/meow_font.h/meow_notif.h 不动;
  build_sdk.sh 自包含版本不动;meow_ui_gfx.c 仅改 meow_font.h include 路径;
  meow_ui_internal.h 去除 FreeType 内部声明
- meow_ipc.h 同步到主仓 launcher 版本(含 AUDIO_FMT_DOP/媒体歌词/蓝牙输出协议)
- 不含 FreeType 矢量字 meow_ft_*(需 freetype vendor,公开仓以位图 meow_font
  自包含);不含 mipsel 二进制 / .o / .a / 固件 / 密钥 / devdocs

examples/hello/:
- 新增 ui/demo/sdk2/sdk2.c;build_hello.sh 纳入该文件(保持自包含)

docs / README / .gitignore:
- docs/MEOW_APP_SDK.md 补 §12.5(默认 MD3 容器 + 底部上滑回桌面 + 两个新 API)
- README 能力清单补「默认 MD3 容器」与「底部上滑回桌面」
- .gitignore 补新增宿主测试二进制

验证:sdk/meow_ui.a 与 examples/hello/hello 交叉编译均 0 warning;工作区无生成物。
2026-09-04 18:05:36 +08:00
AskaEth a1687486bc sdk: full-sync M7.0a/b (theme view input gfx 等, 真实功能非FT) 2026-09-03 06:51:44 +08:00
AskaEth a70cdbc0f7 sdk: 补同步 swipe-delete 缺失文件(widget/widgets.h/internal.h/build_sdk.sh) 2026-09-03 05:59:59 +08:00
AskaEth 64cde13c8d sdk: 同步主仓新增模块 cachegov/lazygrid/select/net 通道 2026-09-03 05:09:05 +08:00
AskaEth 0af6af7c7e docs: 同步主仓最新 SDK 设计/开发者指南 + README 新能力清单
MEOW_APP_SDK.md(相对主仓 MEOW_APP_SDK.md,保留公开仓化改写与免责声明):
- 公共头模块索引补 meow_ui_str.h 行
- §5.5 新增「虚拟化层」(W_LISTVIEW 固定 item 池/多行富 item/自绘钩子;去除 ncm 引用)
- §5.8 新增 TopAppBar 位图动作图标 + sb_icons[] 30 图标索引表
- §6.1 后插入 §6.4 事件分发形式化(命中/捕获/消费/层间隔离/唯一焦点)
- 新增 §12 布局/动画/滚动(meow_box/meow_anim/meow_scroll)+ §13 字符串 i18n(meow_str_*)
- 遗留/待实现 由 §12 重编号为 §14;去除 ncm/网易云/NetEase 字样

APP_DEV_GUIDE.md:新增 §7.11 字符串资源/i18n(保留既有公开化改写 §1.1/§5.2/§10 等)

README.md:更新 include/ 子头计数(20),补「SDK 能力速览」(布局/动画/滚动/事件分发/
虚拟列表/i18n/图标库/图片降采样)

.gitignore:新增本次引入的宿主测试二进制名(test_meow_ui_box/anim/dispatch/listvirt/scroll/str)
2026-09-02 09:17:20 +08:00
AskaEth 4b094d62a5 sdk: 增量同步主仓新模块(box/anim/scroll/dispatch/listvirt/str/sb_icons/img)
subtree pull sdk 来自主仓 pub/sdk(主仓 commit 68ce46a 等)。新增:
- 布局引擎 meow_ui_box / 动画基座 meow_ui_anim / 滚动物理 meow_ui_scroll
- §6.4 事件分发形式化 / W_LISTVIEW 虚拟化多行 / meow_str 字符串资源 i18n
- sb_icons 动作图标扩充 + topbar 位图图标 / meow_img_downscale
- tools/ 图标生成脚本与 SVG 源
- 对应宿主 test_*.c 与 demo_box_scroll.c

build_sdk.sh 冲突解析:保留公开仓自包含版(vendored meow_font + -Iinclude -I.),
OBJS 手工并入 4 个新模块(meow_ui_box/anim/scroll/str)。保留 vendored meow_font.c/
meow_ipc.h/meow_font.h/meow_notif.h,剔除宿主测试二进制与编译产物。
2026-09-02 09:08:51 +08:00
AskaEth 66e9cd5fa4 fix(ncm §32): 内存瘦身修 OOM 被杀 —— 封面降采样(8.28MB→0.78MB) + 瞬态 rbuf 缩减 + 线程栈显式
真机 dmesg 实锤:OOM Kill ncm (score 460, anon-rss 26.9MB, 设备 56MB)。
画像:封面全分辨率 RGBA 8.28MB(1448x1448 原生解码驻留) 是最大可控块。

瘦身(按收益排序):
1. sdk meow_ui_img 新增纯增量 meow_img_downscale + cover.c cover_cap(≤480px)
   → 封面 8.28MB→0.78MB,-7.65MB(mem_sim 实测 PASS)。
2. ncm_api.c 瞬态响应 rbuf:song_url→64KB、lyric/song_detail/cloudsearch/user_playlist→256KB
   (detail/track_all 保 1MB)。
3. ncm_app_internal.h 新增 ncm_thread_create(显式 128KB 栈),替换全部 14 处 pthread_create。
4. 滑动窗口/meta 复核零回退(window_sim:608B/首,6000 首 280KB 恒定)。

验证:mem_sim 封面收益 PASS + host home/player 渲染 OK + stream_sim/player_flow_sim/
window_sim/search_sim/cache_evict_sim 全 PASS + test_ncm_core host+mipsel(qemu) PASS
+ build_ncm.sh -Wall -Wextra 0 warning。真机 scp 播放在跑确认(缓存曲,0 播放请求)。

新 ncm md5 44fa16fe(text 1015150/bss 1362500)。正式部署留主会话。
2026-09-02 03:43:42 +08:00
AskaEth f44d702124 sdk: sb_icons 扩充动作图标 + TopAppBar 位图图标支持
TopAppBar 动作项此前用字体码点(⚙↻⌕)绘制,4k 字库无这些码点→豆腐。改走位图:
- sb_icons.h 追加 15 个 Material Symbols Rounded 动作位图(settings/refresh/search/
  play_arrow/pause/skip_next/skip_previous/arrow_back/close/add/check/lyrics/
  queue_music/download/heart),FILL=1 实心与状态栏一致;sb_icons[] 只追加(0-14 不变),
  动作索引 15-29。
- 生成脚本 tools/gen_action_icons.py(cairosvg svg→32x32 单通道 alpha,SVG 缓存
  icons_src/ 可复现、断网可跑)。
- meow_ui_gfx.c 抽取图标 blit 像素核心 icon_blit_px,新增 meow_gfx_blit_icon_ink
  (控件路径不标顶栏脏区);meow_gfx_blit_icon 状态栏路径行为不变。
- meow_ui_widget.c W_TOPAPPBAR 项支持位图模式:新 API meow_topappbar_set_item_icon
  (w, idx, icon_idx),绘制取主题 onSurface/按压向栏底色淡 12%,文本项行为不变;
  hit 用统一项宽。
- 公共头 widgets.h 加 MEOW_ICON_*(15-29) 枚举与 set_item_icon 声明;internal.h 声明
  meow_gfx_blit_icon_ink。
- 宿主测试(test_meow_ui_widgets)加 topbar 位图图标绘制/命中/按压变淡 + 状态栏 blit
  回归断言;test_umbrella 加 MEOW_ICON_* 编译期锁;MEOW_APP_SDK.md 补说明+索引表。
- 验证:宿主测试 ALL PASS(14项);交叉 zig 构建 0 warning;meow_ui.a 新 md5
  1d69cdae55ab5dde3a98f5e2124f3656。
2026-09-02 03:24:46 +08:00
AskaEth 2e8fac2a07 sdk(ui): 字符串资源外置(i18n 雏形 §13)—— meow_str_* + 内嵌双语表 + TF 覆盖表 + 控件接入
公开 SDK 前把 SDK/App 硬编码文案外置:新增 meow_str/meow_strf/meow_str_set_locale/
meow_str_locale,按当前 locale 取串(永不 NULL),第三方开发者可做多语言。

- 新增 include/meow_ui_str.h + meow_ui_str.c:取串优先级 = TF 覆盖表 > 内嵌当前语言
  (en_us→en 其余→zh) > 内嵌内置默认(zh_cn) > 原样返回 id。内嵌双语表(common.* 21 条 +
  ime.* 9 条);热路径零 malloc(编译期静态表 + ≤256 静态覆盖数组);纯 libc 宿主可单测。
- locale 存系统设置 settings.conf 的 ui.locale(默认 zh_cn,首次调用惰性读取);
  meow_str_set_locale 即时切换 + 重载覆盖表 + best-effort 持久化(流式改写保留其它行)。
- TF 覆盖表 MeowData/<pkg>/strings/<locale>.txt(id=text 行,UTF-8,'=' 两侧去空白);
  无卡/无文件/无该 id → 回落内嵌。meow_strf 用调用方缓冲(snprintf 语义,重入安全)。
- SDK 控件接入:dialog 默认按钮 OK/CANCEL + INPUT 占位、IME 键面/徽标(空格/确定/拼英/
  ?123/清/拼音/(无候选)/中)改走 meow_str;空态/加载态给 App 提供 common.no_data/loading。
- 测试:新增 test_meow_ui_str.c(内嵌回退/TF 覆盖优先级/未知 id 回退/切 locale 即时生效/
  查找正确性/meow_strf,tmp+env 两模式);全量宿主回归(widgets/dispatch/box/anim/scroll/
  listvirt/theme/ime_fix/pinyin/overlay/notif/sdk_input_keys/p1p2p3/config/vol/sb_collapse/
  notify_*) 0 回归;交叉 zig-mipsel -Wall -Wextra 0 warning。
- 文档:MEOW_APP_SDK.md 新增 §13(字符串资源/i18n,遗留顺延为 §14);docs/APP_DEV_GUIDE.md
  新增 §7.11。各宿主 test_*.c 头注释补 meow_ui_str.c 编译依赖。

边界(§13.7):仅 zh_cn/en_us 整串取用;不做复数规则 / BiDi / RTL / 动态字体回退(雏形)。
2026-09-02 02:37:38 +08:00
AskaEth 2d974bed62 sdk(ui): W_LISTVIEW 虚拟化层 — 固定 item 池 + 多行富 item + 完全自绘 (§5.5)
在既有 W_LISTVIEW (src2 每帧取行) 之上叠加虚拟化层 (API 追加式, 旧 API 一字不改):
- 数据源 count_fn + bind_fn (index, meow_lv_item*, ctx); count 每帧实时查 (任意长度≥65535,
  不常驻 item 对象); bind 仅在行进入视口时被调一次
- 固定 item 池: 视口行数+2 静态槽 (有界 ≤96), 回滚不重绑 (同 index 命中同槽); 整表扫过后
  wrap 回收合法但每次 draw 有界 (≤视口窗口)
- 多行富 item: 主标题/副标题/左图标·缩略图/右尾件; 行高 set_item_h(px) 或 set_item_tier(1→48/2→56);
  默认 renderer 主题语义色绘制; set_item_draw 完全自绘; section 段头占位行(非粘性); 空态占位
- 集成: tap_at→meow_ui_focus_acquire (§6.4); meow_scroll_wrap(lv,1) track+fling+snap(行边界);
  选中/按压态走主题; enabled=0 变淡不响应
- 兼容: src2==NULL 走既有路径; W_LIST/W_LISTVIEW 单行用法零改动

验证(宿主): 新测试 test_meow_ui_listvirt T1-T5 全 44 PASS (10000 项滚动窗口内容正确/hit 映射/
bind 有界=count/回滚不重绑/多行渲染/截断/图标占位/段头/空态/自绘/焦点/fling 吸附);
全量回归 widgets/dispatch/box/anim/scroll/ime/pinyin/overlay/notif/sdk_input_keys/p1p2p3 全 PASS;
交叉 zig-mipsel -Wall -Wextra 全 SDK TU 0 warning; meow_ui.a 新 md5=2a2d8000b95dd211d09dc736d5538ad0

文档: MEOW_APP_SDK.md §5.5 同步虚拟化层(API 一览/池语义/集成/回归).
2026-09-02 01:36:48 +08:00
AskaEth 63ac0fd7dc sdk: 事件分发形式化(§6.4)—— 统一命中/捕获/消费/层间隔离/焦点
根治「焦点/事件穿透」一类反复 bug。此前命中/捕获/消费/层间隔离各处 ad-hoc;
现上收为单一仲裁者(meow_ui_input.c),公共 API 只追加、不删改既有签名。

分发模型:
- 触摸沿 z 序自顶向下命中(meow_draw_hit_topdown,owner==active+enable 过滤);
- 命中即消费;模态层永远先审(meow_ui_modal_begin/end/active/depth);
- down 捕获(meow_ui_gesture_begin/owner/end):命中目标捕获手势直到 up,防滑出误触下层;
- 模态存在时下层完全收不到事件——hit_topdown 恒返回模态哨兵(背板消费,不透传);
- meow_ui_run 在 TOUCH_DOWN 自动 begin、TAP/SWIPE/LONG_PRESS/NOTIF_PULL 自动 end。

焦点模型:
- 唯一持有者(meow_ui_focus_acquire/release/owner/tap);
- 点非 focus 区域→释放并通知(meow_ui_focus_tap,list 焦点残留根治);
- 滚动容器(list/listview)tap_at 命中行自动 acquire,点外由 run 的 focus_tap 释放;
- lost 回调(meow_ui_focus_set_lost_cb)通知转移。

迁移(行为修复,正常交互语义不变):
- dialog/sheet → meow_ui_modal_begin/end(模态期下层收不到;叠黑由既有整帧快照+卡片仅重绘保证);
- list/listview tap → meow_ui_focus_acquire;
- 注入/叠加让位(overlay_input_suppressed)与模态层隔离互补,语义不变。

验证:新穿透矩阵 test_meow_ui_dispatch(M1 模态点下层不触发 / M2 弹层叠开两层层隔离 /
M3 捕获手势滑出不误触 / M4 list 焦点点外释放)ALL PASS;
全量回归 widgets/ime_fix/pinyin/overlay_suppress/notif_gesture/sdk_fixes/sb_collapse/
p1p2p3_repro/box/anim/scroll/sdk_input_keys 全 PASS;交叉 -Wall -Wextra 0 warning。

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

No files matched your search

+31
View File
@@ -4,7 +4,26 @@
# ---- 宿主测试二进制(SDK test_*.c 编译出的可执行;源码 .c 仍跟踪)----
test_ime_fix
test_meow_cachegov
test_meow_img_color
test_meow_lyric
test_meow_lyric_view
test_meow_md3
test_meow_ui_anim
test_meow_ui_home_gesture
test_meow_ui_iconbtn
test_meow_ui_lazygrid
test_meow_ui_select
test_meow_ui_swipe
test_meow_ui_theme_export
test_meow_ui_view_event
test_net_ipc
test_meow_ui_box
test_meow_ui_config
test_meow_ui_dispatch
test_meow_ui_listvirt
test_meow_ui_scroll
test_meow_ui_str
test_meow_ui_theme
test_meow_ui_vol
test_meow_ui_widgets
@@ -25,6 +44,18 @@ test_umbrella
examples/hello/hello
examples/hello/render_test*.png
# ---- 图库产物 ----
tuku/picbrowser
tuku/*.mpk
tuku/build/
tuku/test/test_library
tuku/test/test_reader_core
tuku/test/test_settings_reader_persist
tuku/test/test_settings_lock_host
tuku/test/test_pin
tuku/test/test_zip
tuku/test/test_cbz
# ---- 杂项 ----
*.orig
*.log
+35 -1
View File
@@ -22,7 +22,7 @@ MeowOS 应用框架 SDK 的**公开仓库**——面向第三方开发者,让
MeowOS-SDK/
├── sdk/ # meow_ui 应用框架静态库源码(自包含,可交叉 → sdk/meow_ui.a)
│ ├── meow_ui.h # 伞头(#include 聚合 include/ 子头;App 只 include 这个)
│ ├── include/ # 16 个模块子头 + meow_ipc.h / meow_font.h / meow_notif.h(协议/字体)
│ ├── include/ # 24 个模块子头(含 box/anim/scroll/str/lazygrid/select/cachegov/net 通道等)+ meow_ipc.h / meow_font.h / meow_notif.h(vendored)
│ ├── meow_font.c # 预渲染位图字体运行时(meow_font.a 源码,自包含)
│ ├── build_sdk.sh # 交叉编译 meow_ui.a(mipsel 静态库,内含 meow_font.o)
│ └── test_*.c # SDK 宿主逻辑测试(test_host_stub.c 打桩,无硬件可跑)
@@ -31,6 +31,12 @@ MeowOS-SDK/
│ ├── ui/ # 每页一个 view 文件(home/demo/list/full/chrome)
│ ├── logic/ # 纯 C 业务逻辑(不依赖 meow_ui.h,宿主可裸测)
│ └── build_hello.sh # 构建 hello(链 meow_ui.a + libz.a + -lpthread)
├── tuku/ # 图库:漫画书库 App(M3 骨架:主题同步/目录浏览/阅读器桩)
│ ├── app.c # main + 装配 + meow_ui_run 回调路由 + 主题监听
│ ├── logic/ # 纯 C:目录扫描/自然排序、阅读方向状态(宿主可裸测)
│ ├── ui/ # library / reader / settings / lock 每页一个 view
│ ├── build_tuku.sh # 交叉构建(SDK 用 ${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk})
│ └── pack_tuku.sh # 出包 os.meow.picbrowser.mpk
└── docs/
├── APP_DEV_GUIDE.md # 第三方开发上手指南(环境 → 最小 App → 构建打包 → 签名安装 → 调试)
└── MEOW_APP_SDK.md # SDK 设计全量规范
@@ -46,8 +52,36 @@ cd sdk
# 2) 构建示例 App hello
cd ../examples/hello
./build_hello.sh # 产出 examples/hello/hello(静态 MIPS32r2 ELF)
# 3) 构建图库(SDK 用绝对路径,可用 MEOW_SDK_DIR 覆盖)
cd ../../tuku
./build_tuku.sh # 产出 tuku/tuku(静态 MIPS32r2 ELF)
./pack_tuku.sh # 产出 tuku/os.meow.picbrowser.mpk
```
## SDK 能力速览
统一控件 / 输入 / 主题 / 字体 / 通知 / 导航之外,本版 SDK 还提供(详见 [docs/MEOW_APP_SDK.md](docs/MEOW_APP_SDK.md)):
- **布局引擎 `meow_box`**(§12.1):线性布局纯计算器——固定/权重/wrap 尺寸、间隙/边距、主轴与交叉轴对齐、嵌套子 box;`meow_view_set_layout` 与视图框架 opt-in 集成。
- **动画基座 `meow_anim` / `meow_anim_tick`**(§12.2):声明式 tween(linear/ease-out/ease-in-out/overshoot),静态池 ≤16,无动画零开销;只 flush 动画影响区。
- **滚动物理 `meow_scroll`**(§12.3):速度采样 + 松手 fling + 边界 clamp/简单回弹 + snap 吸附;给 `meow_list`/`W_LIST`/`W_LISTVIEW` 加惯性滚动。
- **事件分发形式化**(§6.4):单一仲裁者——z 序自顶向下命中、命中即消费、down 捕获整个手势、模态层隔离、唯一焦点(点外释放/滚动容器转移)。
- **视图事件路由**(§A.7,M7.0a):`meow_view_dispatch_event` 接入 `meow_ui_run`——TAP/KEY/SWIPE/LONG_PRESS 先走视图树 `on_event` 路由,命中即消费(不再落 App `on_*`);无视图树/未命中回落 App 原生 `on_*` 回调(完全向后兼容)。页内子导航按设计定案 b 标注**废弃**(不实现),页内「子页」用 `meow_view_add_child` 挂 CONTAINER 子视图(组合嵌套)。
- **ListView 虚拟化**(§5.5):固定 item 池(视口+2)、多行富 item、段头、空态、完全自绘钩子;App 只提供「总数 + bind」回调,几千/上万行不卡。
- **ListView 左滑删除**(§5.5):列表行横向手势 `meow_listview_swipe_begin/move/end` 三级喂入 + 删除/回弹动画;`meow_listview_set_swipe_delete` 绑删除回调,行自绘 `set_item_draw` 配合画右露删除区(主题 `error/onError`)。
- **圆形图标按钮 `meow_iconbtn_create`**(M7.0a):圆形命中 + 位图图标(`sb_icons[]`/`MEOW_ICON_*`)+ 实时按压/按住态;供电源菜单/播放控制/锁屏件用。
- **主题色板导出/注入**(§8.1.5,M7.0a):`meow_theme_export`/`meow_theme_inject`——「单一权威桥」,导出当前 `style×mode` 解析生效的权威主题(9 角色字段),或注入一份自定义板覆写当前生效主题(`NULL` 恢复内建,覆写随 `meow_theme_reload()` 保留);供 stateless 绘制原语读单一权威、或 launcher/App 自定义背景/强调/风格。
- **字符串资源 / i18n**(§13):`meow_str`/`meow_strf`/`meow_str_set_locale`——内嵌 `zh_cn`/`en_us` 双语表 + TF 卡覆盖表;UI 文案零硬编码,按 locale 取整串。
- **图标库 `sb_icons`**(§5.8):30 个 MD3 Material Symbols 位图动作图标(`MEOW_ICON_*`),`meow_topappbar_set_item_icon` 把 TopAppBar 动作项换成位图图标(修复 ⚙↻⌕ 豆腐)。
- **图片降采样 `meow_img_downscale`**(`include/meow_ui_image.h`):把解码图降到 ≤maxdim 边长省内存(封面/大图用,如 8.28MB→0.78MB)。
- **2 维虚拟化宫格 `meow_lazygrid`**(§12.4):卡片/图标宫格二维虚拟化——只实例化可见行(固定 item 池 ≤96、视口行数+2×cols),`count+bind` 数据源、长列表不卡;命中 `meow_lazygrid_hit/tap_at` 把滚动+网格坐标映射回 item idx;滚动复用 `meow_scroll`(fling/snap)。
- **SDK 默认列表/容器控件默认 MD3**(M6.37–M6.39):`meow_list_create` / `meow_lazycolumn_create` / `meow_lazyrow_create` 与 `meow_listview` **默认即容器模式**——每项 `surfaceContainer` 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,用默认 renderer 的 App 开箱即 MD3;需扁平/满幅分隔线可用 `meow_list_set_container(w,0)` **回退**。
- **全局 App 内底部上滑回桌面**(`meow_ui_input_is_home_swipe`,系统保留手势):任意 App 内屏底边缘上滑即回桌面(后台保活、进程保留转后台、present 自抑),命中优先于 App `on_swipe`/视图树——App **勿在 `on_swipe` 拦截上滑**,否则不生效;中部上滑/滚动(LazyColumn/LazyGrid/页面滚动)不受影响,仍交 App。
- **枚举/预设选择器 `meow_select`**(§5.8.6):开箱即用下拉模态——BottomSheet(标题 + `meow_listview` SINGLE 单选列表)+ `meow_ui_modal_capture/restore` 背景快照(关闭无残影);点选项=选中+`on_change` 回调,点面板外/左缘=取消。
- **磁盘页缓存治理 `meow_cachegov`**(OOM_PAGECACHE_GOVERNANCE):App 登记缓存文件 + read_pos 回调,SDK 做 DONTNEED 治理(L0 曲终整文件丢 / L1 播放中丢已读前缀保 keep_hot 窗口);refault 滚窗逐级回退(放大 keep_hot→降 L0→关 A);`meow_cachegov_download/prefetch_paused` 提供给下载/预取方降载查询。
- **网络自愿通道 `meow_ui_net`**(netmgrd 期 4):联网前 `meow_net_policy_check(host)` 自查是否被策略放行(netmgrd 不在/超时→乐观放行);`meow_net_report(rx,tx)` 自愿上报本进程收发字节(best-effort,失败静默)。
## 构建要求
- **交叉工具链**(mipsel musl 静态):MeowOS 开发机默认路径
+10 -1
View File
@@ -59,7 +59,7 @@ MEOW_SYSROOT="$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linu
SDK 依赖板载 `/dev/fb0` 与字库,宿主机没有——但你可以**链真实 SDK 目标文件 + 打桩**来跑逻辑单测(参考 `sdk/test_host_stub.c`):
- `test_host_stub.c` 提供了 `meow_font_*` / `meow_ui_global_render` 等宿主桩(无 fb → 5×7 点阵回退;无字库 → `meow_font_load` 返回 -1)。
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的,宿主宿主验证全 PASS)。
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的)。注意:公开仓 38 个 `sdk/test_*.c` 的头注释已按实测修订——按注释补齐 `-Iinclude`、`meow_ui_nav.c` 等源集后 **34/38 全部 PASS**;`test_notify_post` / `test_ui_audio_host` / `test_p1p2p3_repro` 依赖主仓组件(launcher/audiod/hello_app),公开仓跳过;`test_net_ipc` 需运行中的 netmgrd,属手工验证项。宿主 `/tmp` 需可写(Termux 本机可用 `proot -b <scratch>:/tmp` 提供)。
- 注意:`logic/` 层要写**纯 C**(不依赖 `meow_ui.h`),才能裸宿主单测;`ui/` 层才碰 SDK。
### 1.3 qemu-mipsel 全系统仿真
@@ -424,6 +424,15 @@ am resolve <pkg> <activity> # 路由决策(返回 CHAIN)
- App 一般不需要联网;确需联网时**克制请求**(见 §8 风控式节制)。
- 系统层面(`netmgrd` 期 0/1):无 iptables 用户态 / 无 cgroup / 无 conntrack → **内核级每应用管控不可行**;主杠杆 = DNS 代理(域名级)+ 连接普查(inet_diag)+ **SDK 自愿通道**。App 走 SDK 自愿通道配合管控是后续方向;当前 App 如联网,请自律、尊重断网降级。
### 7.11 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
- **任何 UI 文案都不要硬编码**,用 `meow_str("域.名")` 按当前语言取串(示例:`meow_str("common.ok")`)。`meow_str` 永不返回 `NULL`:未知 id 原样返回 `id`,方便你打点/兜底展示。
- **`meow_strf(out, outsz, id, ...)`**:取串后按 `snprintf` 语义格式化进调用方缓冲(自带 NUL;`out==NULL`/`outsz<=0` 返回 `-1`)。
- **切换语言**:`meow_str_set_locale("zh_cn"|"en_us")` 即时生效;locale 存系统设置 `settings.conf` 的 `ui.locale`(默认 `zh_cn`,SDK 启动惰性读取)。App 里做语言切换页时调用它即可。
- **覆盖/追加**:可不改二进制在 TF 卡放 `MeowData/<pkg>/strings/<locale>.txt`(每行 `id=text`,UTF-8)覆盖内嵌或新增自己的串(如 `player.pause=暂停`);无卡/无文件/无该 id → 回落内嵌双语默认表(`common.*` + `ime.*` 内置,见 `include/meow_ui_str.h`)。
- **id 规范**:`域.名`;SDK 自带 `common.*`/`ime.*`,你自建 `域`(如 `player.*`)避免撞名。
- **边界**:当前为 i18n 雏形,仅 `zh_cn`/`en_us` 双语**整串**取用;**不做复数规则 / BiDi / RTL / 动态字体回退**(详见 `MEOW_APP_SDK.md §13.7`)。
---
## 8. 规范与红线
+405 -2
View File
@@ -30,6 +30,7 @@
| `meow_ui_config.h` | 应用配置读写 | §9.2 |
| `meow_ui_font.h` | 字体 | §5.7 卡片字体列表 + §10 字体分层 |
| `meow_ui_audio.h` | 独占音频客户端 | §11 |
| `meow_ui_str.h` | 字符串资源/i18n | §13 `meow_str`/`meow_strf`/`meow_str_set_locale` |
其余按需显式 include、**不**随伞头发布的模块头(原样保留、本次未改):`meow_ui_view.h` / `meow_ui_nav.h` / `meow_ui_media.h` / `meow_ui_sys.h` / `meow_ui_vol.h`。`meow_ui_internal.h` 为 SDK 内部件,不随伞头发布。
@@ -373,7 +374,7 @@ int meow_ui_setform_hit_row(meow_widget *f, int x, int y);/* 命中行号(-1=
### 5.5 设计模板 A② 列表+选择 [源:2026-08-30]
> **性质**:SDK 设计模板,供 App 主页/子页按需选用;对既有 `meow_list_create`(B1 最小集,§5,仅 label 文本)做**结构化扩展**(行=图标+标题+副标题+右侧操作 + 选中态)。API「草案/待定案」,不实现。**(✅ 已实现:结构化列表控件 `W_LISTVIEW`,M2.76 · agent-186 · 243148e,见 §5.8.8 分批;长按事件 `MEOW_EV_LONG_PRESS` + `meow_ui_set_long_press_cb/ms`,M2.77 · agent-191 · e95552b。)**
> **性质**:SDK 设计模板,供 App 主页/子页按需选用;对既有 `meow_list_create`(B1 最小集,§5,仅 label 文本)做**结构化扩展**(行=图标+标题+副标题+右侧操作 + 选中态)。API「草案/待定案」,不实现。**(✅ 已实现:结构化列表控件 `W_LISTVIEW`,M2.76 · agent-186 · 243148e,见 §5.8.8 分批;长按事件 `MEOW_EV_LONG_PRESS` + `meow_ui_set_long_press_cb/ms`,M2.77 · agent-191 · e95552b;虚拟化层(固定 item 池 + 多行富 item + 完全自绘 + 段头 + 空态),2026-09-02,见 §5.5「虚拟化层」。)**
**摘要**:可滚动选择容器。**行数据源回调**逐行给出(图标 / 标题 / 副标题 / 右侧操作 / 是否可选),容器负责绘制/滚动/选中/命中;支持**单选、多选、长按**(上下文操作)。适合「已保存网络 / 附近WiFi / 蓝牙设备 / 歌曲列表 / 设置子菜单」等。
@@ -420,6 +421,51 @@ int meow_ui_listview_hit(meow_widget *lv, int x, int y); /* 命中行;
```
> 说明:既有 `meow_list_create`(label-only)保留;`meow_ui_listview_*` 为**结构化扩展**(图标/副标题/右侧/选中),可作为真则替换裸 label 列表。
#### 虚拟化层(2026-09-02,W_LISTVIEW 扩展;API 追加式,旧 API 一字不改)
> **痛点**:W_LISTVIEW(src2 每帧按需取行)只画可见行,但**每帧重取全部可见行**;大列表(几千/
> 上万首)App 只能自行造「滑动窗口」补载(如超长歌单的滑动窗口补载)。
> 本层把「固定 item 池 + 多行富 item + 完全自绘」收编为 SDK 级能力,让 App 只提供「总数 + bind」回调。
**关键交互/数据模型**:
- **数据源**:`meow_listview_count_fn`(返回总行数,任意长 ≥65535,不常驻 item 对象)+ `meow_listview_bind_fn`
(`index, meow_lv_item*, ctx`,仅在行「进入视口」(绑定到固定池槽)时被调**一次**)。`count` 每帧实时查
(搜索过滤/懒加载动态长度)。
- **固定 item 池**:池容量 = 视口行数 + 2 静态槽(有界,`MEOW_LV_POOL_MAX`=96),行离开视口不销毁、
**回滚不重绑**(同一 index 命中同一槽 → bind 不重调);滚动只改 viewport 偏移(复用既有 set_scroll/max_scroll),
不重绑未进视口的项。**整表扫过后池发生 wrap(cap<count),回收合法重绑但每次 draw 有界(≤视口窗口)**。
- **多行富 item**:`meow_lv_item` 含 主标题 / 副标题(第二行小字)/ 左图标·缩略图 / 右尾件(chevron·开关·复选·⋯)/
`section`(段头占位行)/ `enabled`。行高可配:`set_item_h`(精确 px)或 `set_item_tier(lines)`(1→48 / 2→56)。
- **默认 renderer**(`meow_listview_set_item_renderer(lines, show_icon, show_right)`)用主题语义色绘制富 item;
**完全自绘**:`meow_listview_set_item_draw(draw_fn, ctx)` 覆盖默认 renderer(App 收 idx/rect/state 自绘整行)。
- **段头**:bind 返回 `item->section=1`(title=段名)→ 段头占位行(非粘性;粘性为后续增强)。
- **空态**:`meow_listview_set_empty_text(text)`——count==0 时视口内居中画文案。
**与既有机制集成**:焦点/事件走 §6.4(`meow_listview_tap_at` 命中行自动 `meow_ui_focus_acquire`,点外由
`meow_ui_focus_tap` 释放——list 焦点残留根治下沉);滚动接 §12.3 `meow_scroll`(`meow_scroll_wrap(lv, 1)`,
`track`+`fling` 跟手/惯性,`set_snap(item_h)` 吸附到行边界);选中/按压态样式走主题
(`primaryContainer`/半透明 `primary` 按压层);`enabled=0` 行变淡且命中不响应。
**API 一览(`include/meow_ui_widgets.h`,§5.5)**:
```c
typedef struct meow_lv_item { const char *title, *subtitle; const meow_img *icon; int icon_kind;
int right, on, enabled, section; void *user; } meow_lv_item;
typedef int (*meow_listview_count_fn)(void *ctx);
typedef void (*meow_listview_bind_fn)(int index, meow_lv_item *item, void *ctx);
typedef void (*meow_listview_draw_fn)(int idx, int x, int y, int w, int h, int state, void *ctx);
int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count,
meow_listview_bind_fn bind, void *ctx); /* 切 pool 模式 */
int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon, int show_right);
int meow_listview_set_item_tier(meow_widget *lv, int lines); /* 1→48 / 2→56 */
int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw, void *ctx);
int meow_listview_set_empty_text(meow_widget *lv, const char *text);
```
> 兼容:既有 `meow_listview_*`(src2 模式)全部保留不动(`src2==NULL` 走既有路径);本层为叠加。
> `meow_widget` 尾部追加虚拟化字段(`calloc` 归零,安全);`meow_ui.a` 需重编,App 链新库(老 ABI 不重建名)。
> 宿主验证 `test_meow_ui_listvirt`(T1 虚拟化/bind 有界/回滚、T2 多行/截断/图标/尾件、T3 段头/空态、
> T4 自绘、T5 焦点/fling 联动)+ 全量回归 widgets/dispatch/box/anim/scroll/ime/pinyin/overlay/notif/
> sdk_input_keys/p1p2p3 全 PASS;交叉 -Wall -Wextra 0 warning。
---
### 5.6 设计模板 B③ 弹窗/对话框 [源:2026-08-30]
@@ -755,6 +801,58 @@ meow_widget *meow_layout_spacer(int w, int h);
---
> **TopAppBar 位图动作图标(M2.9x,追加式)**:TopAppBar 动作项默认是**文本**(`meow_topappbar_add_action`)。为支持「⚙↻⌕」类**动作图标**(4k 字库无这些码点,字体码点画图标会出豆腐),新增把某动作项改为**位图图标**的接口:
>
> ```c
> int meow_topappbar_set_item_icon(meow_widget *w, int idx, int icon_idx);
> ```
> - `idx` = 动作项下标(`meow_topappbar_add_action` 返回值;越界返回负值)。
> - `icon_idx` = `sb_icons[]` 位图索引(用 `MEOW_ICON_*` 常量,见下表;<0 非法)。
> - 返回 `0`=成功;`NULL`/非 TopAppBar/越界 `idx`/非法 `icon_idx`=负值。
> - 绘制:与状态栏**同源**位图(Material Symbols Rounded 32x32 单通道 alpha,`meow_gfx_blit_icon_ink`),
> 图标槽在动作格内水平居中、ink 垂直对齐栏中点,颜色取主题 `onSurface`、**按压态向栏底色淡 12%**。
> - **兼容**:未调用本 API 的动作项保持文本(行为不变);`items` 文本条目不感知位图化。
>
> **sb_icons[] 索引表**(Material Symbols Rounded,MD3,单通道 alpha 32x32;Apache-2.0,
> 来源 google/material-design-icons,重新生成 `sdk/tools/gen_action_icons.py`):
>
> | 索引 | 状态栏 `SB_I_*`(SDK 内部) | TopAppBar `MEOW_ICON_*`(App 用) | 图形 | 用途 |
> |---|---|---|---|---|
> | 0 | `SB_I_WIFI` | — | Wi-Fi | 状态栏 WiFi(满格) |
> | 1 | `SB_I_BT` | — | 蓝牙 | 状态栏蓝牙(连接中) |
> | 2 | `SB_I_BATT` | — | 电池 | 状态栏电量 |
> | 3 | `SB_I_VOL` | — | 音量 | 状态栏音量 |
> | 4 | `SB_I_BRIGHT` | — | 亮度 | 状态栏背光 |
> | 5 | `SB_I_WIFI_STRONG` | — | Wi-Fi | 状态栏(-60dBm 强) |
> | 6 | `SB_I_WIFI_MEDIUM` | — | Wi-Fi | 状态栏(-75dBm 中) |
> | 7 | `SB_I_WIFI_WEAK` | — | Wi-Fi | 状态栏(更弱) |
> | 8 | `SB_I_WIFI_NO` | — | Wi-Fi | 状态栏(开未连接) |
> | 9 | `SB_I_WIFI_OFF` | — | Wi-Fi | 状态栏(关闭) |
> | 10 | `SB_I_BT_NO` | — | 蓝牙 | 状态栏(未连接) |
> | 11 | `SB_I_BT_CONN` | — | 蓝牙 | 状态栏(已连接) |
> | 12 | `SB_I_BT_DIS` | — | 蓝牙 | 状态栏(关闭) |
> | 13 | `SB_I_BT_SEARCH` | — | 蓝牙 | 状态栏(搜索中) |
> | 14 | `SB_I_BATT_CHG` | — | 电池 | 状态栏(充电) |
> | 15 | `SB_I_SETTINGS` | `MEOW_ICON_SETTINGS` | ⚙ 齿轮 | 设置 |
> | 16 | `SB_I_REFRESH` | `MEOW_ICON_REFRESH` | ↻ 回转 | 刷新 |
> | 17 | `SB_I_SEARCH` | `MEOW_ICON_SEARCH` | 🔍 放大镜 | 搜索 |
> | 18 | `SB_I_PLAY_ARROW` | `MEOW_ICON_PLAY_ARROW` | ▶ | 播放 |
> | 19 | `SB_I_PAUSE` | `MEOW_ICON_PAUSE` | ⏸ | 暂停 |
> | 20 | `SB_I_SKIP_NEXT` | `MEOW_ICON_SKIP_NEXT` | ⏭ | 下一首 |
> | 21 | `SB_I_SKIP_PREV` | `MEOW_ICON_SKIP_PREV` | ⏮ | 上一首 |
> | 22 | `SB_I_ARROW_BACK` | `MEOW_ICON_ARROW_BACK` | ← | 返回 |
> | 23 | `SB_I_CLOSE` | `MEOW_ICON_CLOSE` | × | 关闭 |
> | 24 | `SB_I_ADD` | `MEOW_ICON_ADD` | + | 添加 |
> | 25 | `SB_I_CHECK` | `MEOW_ICON_CHECK` | ✓ | 勾选 |
> | 26 | `SB_I_LYRICS` | `MEOW_ICON_LYRICS` | 歌词 | 歌词 |
> | 27 | `SB_I_QUEUE_MUSIC` | `MEOW_ICON_QUEUE_MUSIC` | 队列 | 播放列表 |
> | 28 | `SB_I_DOWNLOAD` | `MEOW_ICON_DOWNLOAD` | ↧ | 下载 |
> | 29 | `SB_I_HEART` | `MEOW_ICON_HEART` | ♥ | 收藏 |
>
> **索引约定**:状态栏占用 `0-14`(既有序号,**勿改序**);TopAppBar 动作从 `15` 起连续追加(只追加,既有索引不变)。`MEOW_ICON_*`(公共头 `meow_ui_widgets.h`)与 `SB_I_*`(SDK `sb_icons.h`)**同值**,二者须保持表序一致(`test_umbrella.c` 有 `_Static_assert` 锁 15/21/27/29)。
---
### 5.9 音频客户端 API(`meow_audio_*`,M2.90 落地)
> **权威规范:`devdocs/plan/AUDIOD_DESIGN.md`**(用户定案五项决策)。**已实现**:A1(agent-244/246,audiod+CLI+SDK 客户端,M2.90 入固件)+ A2(流媒体客户端迁 SINK_AUDIOD,e013621)+ 后续批次(M2.91 FPGA 开机武装 / M2.93 长流护栏 / M2.94 几何收窄 / M2.95 音量反转 / M2.96 炸耳修复 / 261 电平监控待 M2.97 固件化)。IPC 号段 `0x00C0-0x00CA` 见 `meow_ipc.h` / docs/MEOWOS_SPEC_REF §4。
@@ -881,6 +979,99 @@ void meow_ui_global_remove(int id);
---
### 6.4 事件分发形式化(2026-09-02):命中测试 / 捕获 / 消费 / 层间隔离 / 唯一焦点
> **定位**:把此前各处 ad-hoc 的「命中/捕获/消费/层间隔离/焦点」补丁(模态穿透、弹层叠黑、
> list 焦点残留、同坐标二次消费)**上收为单一仲裁者**。三个历史根因(设置页滑条穿透、
> Dialog/Sheet 弹层叠黑、list 焦点残留导致主页面滑不动)与「注入事件被下层同坐标控件二次消费」
> 都在本节模型内覆盖。**兼容铁律:只追加 API、不删改既有签名**;未调用本节 API 的 App 行为与
> 之前完全一致(命中/捕获仍可由 App 在 `on_tap`/`on_touch_*` 自理)。hello/files/reader 等
> 既有 App 只把 SDK 提供的语义用于**收敛 bug**,不改正常交互语义。
#### 6.4.1 分发模型(自上而下命中 + 命中即消费 + down 捕获 + 模态层间隔离)
触摸事件沿 **z 序自顶向下**命中(模态层**永远先审**);某一层命中即消费(返回 consumed 才继续
下探)。一次 **touch-down 命中后捕获整个手势直到 up**——期间 move/up 只发给捕获目标,**防滑出后
误触发下层**(根治「手指滑出控件仍点下层」)。**模态层存在时下层完全收不到事件**(含兜底:
模态背板未命中的事件由背板消费,不透传)。
层序(顶 → 底)与 §6.1 三层模型 +0 模态层合成一张表:
| 层 | 命中语义 | 是否透传 |
|---|---|---|
| **+0 模态层**(Dialog/Sheet/通知中心/电源菜单等) | 模态始终先审;未命中的事件由**背板消费**(点外=取消/关闭) | **不透传** |
| **③ 系统全局**(IME/状态栏/通知中心入口……已在运行循环消费) | 命中即消费 | 返回 1 才透传 |
| **② App 跨 Activity 全局**(`meow_ui_global_hit`) | 命中返回 1=消费 | 否则落下层 |
| **① 当前 Activity 页面控件**(view/widget,z 降序) | 命中即消费 | 否则让 App 自理 |
SDK 提供统一仲裁者(`meow_ui_hit_topdown` 沿 z 降序返回首个命中控件,含 `owner==active`+enable
过滤)。**模态存在时它恒返回模态哨兵**——下层控件(即使同坐标)绝不会成为捕获/命中目标。
手势捕获由 `meow_ui_gesture_begin/owner/end` 表达:`meow_ui_run` 在 `MEOW_EV_TOUCH_DOWN` 时
自动 `begin`(命中并捕获),随后 move/up 只发给捕获目标(`meow_ui_gesture_owner` 恒定),
抬手(TAP/SWIPE/LONG_PRESS/NOTIF_PULL/清队列)自动 `end` 释放。模态期间 begin 捕获的是模态
哨兵——下层不被捕获,天然根治穿透。
#### 6.4.2 焦点模型(唯一持有者 + 点外释放 + 滚动容器转移)
**焦点持有者唯一**(`meow_ui_focus_owner`)。点非 focus 区域 → **释放焦点并触发通知回调**
(`meow_ui_focus_tap`,list 焦点残留根治)。滚动容器(W_LIST/W_LISTVIEW)在
`meow_list_tap_at`/`meow_listview_tap_at` 命中行时自动 `meow_ui_focus_acquire`(列表成为唯一
焦点);任何落在该容器外的触摸(`meow_ui_focus_tap` 判定 hit != focus owner)释放焦点并通知,
回退到页面级滚动。
**滚动容器间焦点转移规则**:
1. 某滚动容器(list/lazycolumn/row)获得焦点后,随后的**纵向** swipe / 拖动滚动该容器;
2. 任何命中落在该容器**外**的触摸(含空白、页面内容、其它控件)→ 焦点释放 + `lost` 回调,
后续拖动滚动**页面**(不再滚列表);
3. 焦点释放/转移不销毁容器控件(`meow_widget` 仍由 App/视图持有),下次命中行重新 acquire。
#### 6.4.3 公共 API(至 `meow_ui_input.h`,仅追加)
```c
/* 模态作用域:dialog/sheet/App 自绘模态「打开时 begin、关闭时 end」;支持嵌套(depth 递增)。 */
typedef struct meow_ui_modal meow_ui_modal;
meow_ui_modal *meow_ui_modal_begin(void); /* 打开模态层入栈;返回句柄(NULL=栈满) */
void meow_ui_modal_end(meow_ui_modal *m); /* 关闭:弹到 m(含)一次性关上嵌套;幂等 */
int meow_ui_modal_active(void); /* 当前是否有任一模态作用域 */
int meow_ui_modal_depth(void); /* 模态栈深(弹层叠开计数) */
/* 自上而下命中:z 降序返回首个命中控件;模态存在时恒返回模态哨兵(背板消费,不透传)。 */
void *meow_ui_hit_topdown(int x, int y);
int meow_ui_widget_hit(meow_widget *w, int x, int y); /* meow_widget_hit 公共别名 */
/* 手势捕获:down 命中即捕获直到 up;期间 move/up 只给捕获目标(滑出不误触)。 */
void *meow_ui_gesture_begin(int x, int y);
void *meow_ui_gesture_owner(void);
void meow_ui_gesture_end(void);
/* 唯一焦点:点非 focus 区域 → 释放并通知(list 焦点残留根治)。 */
typedef void (*meow_ui_focus_lost_fn)(void *owner, void *ctx);
void meow_ui_focus_set_lost_cb(meow_ui_focus_lost_fn cb, void *ctx);
void *meow_ui_focus_owner(void);
int meow_ui_focus_acquire(void *owner);
int meow_ui_focus_release(void *owner);
void meow_ui_focus_tap(int x, int y);
```
#### 6.4.4 迁移与兼容
- **dialog/sheet**(`meow_ui_dialog.c`):`meow_dialog_show`/`meow_sheet_open` 已迁到
`meow_ui_modal_begin/end`——模态打开期间 `meow_ui_modal_active()` 为真,下层收不到触摸;
点卡片外由背板消费(关闭)。**弹层叠黑**由既有「打开前整帧快照 + 关闭还原 + 高亮仅重绘卡片
不叠遮罩」保证(M2.71-bug3),与本节模态层隔离正交、互不影响。
- **list/scroll**(`meow_ui_widget.c`):`meow_list_tap_at`/`meow_listview_tap_at` 命中行自动
`meow_ui_focus_acquire`,点外由 `meow_ui_run` 的 `meow_ui_focus_tap` 统一释放——list 焦点残留
根治下沉到 SDK;App 层不再需要各自「点非 list 区域手动清 focus」补丁。
- **注入/叠加让位**:`overlay_input_suppressed`(§10.5)是**系统叠加层**的层间隔离(面板输入由
launcher 接管、App 排空副本),与本节模态层隔离互补共同防「同坐标二次消费」,语义不变。
- 以上均为**行为收敛**(修 bug),正常交互语义不变;`p1p2p3_repro`/`widgets`/`ime_fix`/`pinyin`/
`overlay_suppress`/`notif_gesture`/`sdk_fixes`/`sb_collapse`/`box`/`anim`/`scroll`/`sdk_input_keys`
全量回归通过。
---
## 7. 动画/时间(B1 最小可用)
当前无动画框架。B1 提供**帧驱动回调 + 内置时间**,应用据此做低成本的帧序列/淡入淡出:
@@ -1073,7 +1264,219 @@ patch_v2/meow_launcherd/meow_font.a # 复用(静态链接)
---
## 12. 遗留 / 待实现
## 12. 控件层扩展:可组合布局引擎 / 动画基座 / 滚动物理
> **性质**:三块**独立增量**(对照 §5.8.5 的「固定尺寸 Row/Column/Box」与 §7 的「B1 最小动画」做升级),
> **全部追加式**:新头 `include/meow_ui_box.h` / `include/meow_ui_anim.h` / `include/meow_ui_scroll.h` +
> 新 TU `meow_ui_box.c` / `meow_ui_anim.c` / `meow_ui_scroll.c`;**不改任何既有 API 签名/行为**。
> 既有控件仍是**绝对定位**(x,y,w,h);布局只是「帮你算坐标」的容器,不改变绘制/命中/状态机。
> 旧绝对坐标**永远有效**——不用这三块的 App 完全照旧。
### 12.1 线性布局引擎(`meow_box`)
在既有 `meow_row/column/box`(§5.8.5,只支持固定尺寸+居中)之上,提供更完整的线性布局:
- **容器**:竖直(`MEOW_BOX_V`)/ 水平(`MEOW_BOX_H`)。
- **子项尺寸策略**:固定(`MEOW_BOX_FIXED`,取控件当前 w/h)| 权重(`MEOW_BOX_WEIGHT`,均分剩余主轴空间)| wrap(`MEOW_BOX_WRAP`,按内容测量)。
- **间距/边距**:容器 `padding`(left/top/right/bottom)+ 项间 `gap`;子项 margin 读取既有 `meow_widget_opts.margin_x/y`。
- **主轴对齐**:start / center / end / space-between / space-around / space-evenly。
- **交叉轴对齐**:start / center / end / stretch(填满容器交叉向减 margin)。
- **嵌套**:box 内可再挂子 box(`meow_box_add_sub`),求解深度 ≤ `MEOW_BOX_MAX_DEPTH`(16,静态栈,无递归爆炸)。
```c
typedef struct meow_box meow_box;
typedef enum { MEOW_BOX_H = 0, MEOW_BOX_V = 1 } meow_box_dir;
typedef enum { MEOW_BOX_FIXED = 0, MEOW_BOX_WEIGHT = 1, MEOW_BOX_WRAP = 2 } meow_box_size;
typedef enum {
MEOW_BOX_ALIGN_START = 0, MEOW_BOX_ALIGN_CENTER, MEOW_BOX_ALIGN_END,
MEOW_BOX_ALIGN_SPACE_BETWEEN, MEOW_BOX_ALIGN_SPACE_AROUND, MEOW_BOX_ALIGN_SPACE_EVENLY,
} meow_box_align;
typedef enum { MEOW_BOX_CROSS_START = 0, MEOW_BOX_CROSS_CENTER, MEOW_BOX_CROSS_END, MEOW_BOX_CROSS_STRETCH } meow_box_cross;
meow_box *meow_box_new(meow_box_dir dir); /* 命名避开既有 meow_box_create(W_LAYOUT) */
void meow_box_free(meow_box *b);
void meow_box_set_rect(meow_box *b, int x, int y, int w, int h); /* 容器矩形(绝对坐标) */
void meow_box_set_padding(meow_box *b, int l, int t, int r, int btm);
void meow_box_set_gap(meow_box *b, int gap);
void meow_box_set_align(meow_box *b, int main_align, int cross_align);
int meow_box_add(meow_box *b, meow_widget *w, int size, int weight); /* 固定/权重 */
int meow_box_add_ex(meow_box *b, meow_widget *w, int size, int weight,
int min, int preferred, meow_box_measure_fn m, void *mctx); /* WRAP 测量 */
int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight); /* 嵌套 */
/* 单遍求解:把每个子控件 x/y/w/h 写进 meow_widget(新增 meow_widget_set_rect);深度≤16 静态栈。
* 子控件 margin(widget opts.margin_x/y)参与排布;嵌套 sub 递归。 */
void meow_box_arrange(meow_box *b);
```
- **与视图框架集成(opt-in)**:box 可作 `meow_view` 的布局提供者;view **不设**就保持现有绝对坐标行为。
`meow_view_set_layout(v, box)` 登记 + 立即排一次;`meow_view_relayout(v)` 手动重排;
顶层页在 `meow_view_page_push/pop/replace` 进入时自动重排。box 矩形默认跟 view 内容矩形(w/h 未设时自动填)。
```c
void meow_view_set_layout(meow_view *v, meow_box *box); /* NULL=取消 */
void meow_view_relayout(meow_view *v);
```
- **控件尺寸测量**:`MEOW_BOX_FIXED`/`WRAP`(无 measure 回调)直接取控件当前 `w/h`(对既有固定尺寸控件天然兼容);
`WRAP` 可用 `meow_box_measure_fn`(给出主轴向 min/preferred)供内容自适应的自定义控件用。
- **内存纪律**:box 静态池、定长子项数组(`MEOW_BOX_MAX_CHILD=24`),arrange 热路径零 malloc;嵌套用深度≤16 静态栈。
### 12.2 动画基座 + UI 节拍器(`meow_anim` / `meow_anim_tick`)
把 §7「应用在 on_tick 里用 `meow_ui_time_ms` 做时间轴」收敛成**声明式 tween**:
- **缓动**:linear / ease-out / ease-in-out / overshoot(`meow_ease_apply`)。
- **动画对象**:`meow_anim_start` 一次性声明 from/to/duration/easing + `update`(每帧插值回调)+ `done`(完成/取消回调);**并行多动画**(静态池 ≤ `MEOW_ANIM_MAX=16`)、**取消**(`meow_anim_cancel`)。
- **节拍**:`meow_anim_tick()` 挂既有 `meow_ui_run` 主循环(on_tick 之后)自动调用;**无动画时零开销**(只查计数即返回),不引入常驻高频轮询。阻塞 IME 循环(`meow_ui_feed_tick`)不喂 tick(动画在模态期暂停)。
- **与脏区集成**:`meow_anim_set_region(a,x,y,w,h)` 让每帧**只 flush 动画影响区**,不做整屏重绘;app 的 `update` 回调也可自行 `meow_ui_flush_region`。
```c
typedef enum { MEOW_EASE_LINEAR = 0, MEOW_EASE_OUT, MEOW_EASE_IN_OUT, MEOW_EASE_OVERSHOOT } meow_ease;
float meow_ease_apply(meow_ease e, float t); /* t∈[0,1]→[0,1];overshoot 中后段可略>1、末点=1 */
typedef struct meow_anim meow_anim;
typedef void (*meow_anim_update_fn)(meow_anim *a, float v, void *ctx);
typedef void (*meow_anim_done_fn)(meow_anim *a, int canceled, void *ctx);
typedef struct meow_anim_desc {
float from, to; uint32_t duration_ms; meow_ease ease;
meow_anim_update_fn update; meow_anim_done_fn done; void *ctx;
} meow_anim_desc;
meow_anim *meow_anim_start(const meow_anim_desc *d);
void meow_anim_cancel(meow_anim *a);
int meow_anim_active(meow_anim *a);
float meow_anim_value(meow_anim *a);
void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h);
void meow_anim_tick(void); /* 推进所有活动动画(挂主循环;无动画零开销) */
unsigned meow_anim_count(void);
```
- **与 §7 的关系**:§7(B1 最小可用)是手写时间轴;本模块是正式基座。应用可任选其一或混用。
- **内存纪律**:静态池 ≤16,start/cancel/tick 全程零 malloc。
### 12.3 滚动物理(`meow_scroll`)
给既有滚动容器(`meow_list` / `W_LIST` lazycolumn/lazyrow / `meow_listview`)加**可惯性滚动**物理层:
- **速度跟踪**:`meow_scroll_track(dy, t)`(拖动跟手 + 采样),fling 时用**最近 N 个采样加权**得初速。
- **松手 fling**:`meow_scroll_fling(s)` 起一条减速度曲线动画(用 §12.2 动画基座驱动)推进位移;每帧**只 redraw 列表视口区**(不整屏)。
- **边界过冲 clamp + 简单回弹**:free 目标越界 → clamp 到 `[0,max]`;快速撞墙补两段短 IN_OUT 回弹(不做复杂 spring)。
- **滚动停止自动归位**:`meow_scroll_set_snap(step)` 可选吸附到 step 倍数。
- **跟手拖动行为保持现状不变**:`track` 内部调既有 `meow_list(viewport)_set_scroll`(clamp 一致),与 app 直接 `scroll_by` 等价;app 把拖动的 `scroll_by` 换成 `track` 即可获得速度采样。
```c
typedef struct meow_scroll meow_scroll;
meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
meow_scroll_get_fn get, meow_scroll_redraw_fn redraw, void *ctx);
meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview); /* 1=W_LISTVIEW / 0=W_LIST 或 lazycolumn/row */
void meow_scroll_free(meow_scroll *s);
void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms); /* 跟手拖动 + 速度采样 */
void meow_scroll_fling(meow_scroll *s); /* 松手惯性滑动(内部算初速) */
void meow_scroll_stop(meow_scroll *s); /* 立即停止 */
int meow_scroll_velocity(meow_scroll *s); /* 加权初速 px/s */
int meow_scroll_active(meow_scroll *s);
int meow_scroll_offset(meow_scroll *s);
void meow_scroll_set_snap(meow_scroll *s, int step);
void meow_scroll_set_friction(meow_scroll *s, int tau_ms); /* 默认 180ms */
```
- **内存纪律**:静态池(应用侧按需建会话),track/fling/tick 全程零 malloc;无 fling 时 meow_scroll 不参与节拍(由 `meow_anim_tick` 活动计数保证零开销)。**不引 libm**(SDK 免 `-lm` 约定)。
### 12.4 对既有 App 的零影响(迁移说明)
- **追加式**:三块全为新 API;既有 `meow_row/column/box`(§5.8.5)与全部 `meow_*_create/hit/scroll` 签名一行未改。
- **旧绝对坐标永远有效**:box 只是把坐标算进 `meow_widget`(`meow_widget_set_rect`);不用 box 的 App 照旧 `on_layout`/绝对定位。
- **机制层新增**:`meow_ui_input.c` 主循环新增 `meow_anim_tick()` 调用(无动画零开销);`meow_ui_view.c` 新增布局登记表(未设布局的 view 行为不变);`meow_ui_widget.c` 新增 `meow_widget_set_rect`(只设字段不标脏,既有 `meow_layout_arrange` 不受影响)。
- **同步重编注意**:`meow_ui.a` 内容变化(新增 box/anim/scroll 三个 `.o`,`meow_ui_widget.o`/`meow_ui_view.o`/`meow_ui_input.o` 重编)→ 链 `meow_ui.a` 的 App 需**重链**(`build_sdk.sh` 一键重编 + 各 App `build_*.sh` 重链)。既有 App **不改源码**即可零回归。
### 12.5 SDK 默认列表/容器控件默认 MD3(`meow_list_set_container`)+ 全局 App 内底部上滑回桌面
> **性质**:M6.37–M6.39 批,两条默认行为定案。
> ① **SDK 默认列表/容器控件默认 MD3**——`meow_list_create` / `meow_lazycolumn_create` / `meow_lazyrow_create`(皆 `W_LIST`)
> 与 `meow_listview` 容器控件**默认即容器模式**:每项套 `surfaceContainer` 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,
> 用默认 renderer 的 App **开箱即 MD3**(此前默认扁平)。需扁平/满幅分隔线的宿主可显式 `meow_list_set_container(w,0)` **回退**。
> ② **全局 App 内「底部上滑回桌面」为系统保留手势**——任意 App 内屏底边缘上滑即回桌面(后台保活、进程保留转后台),
> App **勿在 `on_swipe` 拦截上滑**,否则该手势不生效。
```c
/* MD3 容器模式(W_LIST 共用):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
int meow_list_set_container(meow_widget *w, int on);
/* 底部热区上滑 → 回桌面(home)判定(镜像 launcher np_gesture.h 同口径):屏底热区起滑 +
* 上移 ≥HOME_SWIPE + 竖向主导(|dx|<|dy|)+ 避开左缘返回列。1=命中(系统保留手势);0=否。 */
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h);
```
- **MD3 默认**:`container` 字段默认 1;`meow_list_set_container(w,0)` 回退至「满幅扁平行 + 分隔线」旧观感。
容器模式下未选中行以 `surfaceContainer` 呈现、选中 `primaryContainer`、按压 `primary α26`;分隔线去除,回退时以 `onSurfaceVariant` 分隔线。
宿主测试 `test_meow_ui_widgets`/`test_meow_ui_listvirt` 用 `set_container(0)` 锁扁平基线。
- **业务上滑不冲突**:`meow_ui_input_is_home_swipe` 只认「**屏底热区 + 竖向主导上滑**」;**中部上滑/滚动
(LazyColumn/LazyGrid/页面滚动)不受影响**,仍交 App;底部热区横向滑仍走左缘返回/横向逻辑。
- **App 勿拦截**:命中后 SDK 作为**系统保留手势**优先于 App `on_swipe`/视图树(否则已注册 `on_swipe` 的 App 会接管,
导致 App 内上滑不回桌面),路由 `meow_ui_nav_request_exit()`(MSG_NAV EXIT → launcher 批①后台保活,进程保留转后台);
**不**调 `meow_ui_request_exit()`(那会 `exit(0)` 断送保活)。IME 软键盘覆盖底部热区时手势让位 IME(防输入时误退)。
- **系统边界**:与「顶部下拉通知中心」同理为系统保留手势;流媒体客户端/媒体 App 的列表、播放页照常用**中部**手势
(滚动/翻页),不外溢到屏底热区拦截回桌面。
---
## 13. 字符串资源 / i18n(`meow_str_*`,2026-09-02 追加)
> **性质**:字符串资源外置(i18n 雏形)——公开 SDK 前,SDK/App 的 UI 文案不再硬编码在 C 源码:第三方开发者按系统 locale 取对应语言串,且可不改 SDK 二进制用 TF 卡覆盖/追加。**追加式**:新头 `include/meow_ui_str.h` + 新 `.o` `meow_ui_str.c`;既有 `meow_ui.h` 伞头自动聚合(仅新增 #include),既有 API/控件签名一行未改。
### 13.1 API(追加式,纯 libc,宿主 gcc 可直接单测)
```c
const char *meow_str(const char *id); /* 按当前 locale 取 id 文案;静态串,永不 NULL;未知 id 原样返回 id */
int meow_strf(char *out, int outsz, const char *id, ...); /* 取串后按 snprintf 语义格式化进 out(自带 NUL);out==NULL/outsz<=0→-1 */
void meow_str_set_locale(const char *locale); /* 切 locale("zh_cn"|"en_us"),即时生效 + 重载 TF 覆盖表 + best-effort 持久化 ui.locale */
const char *meow_str_locale(void); /* 当前生效 locale(缺省 "zh_cn") */
```
> `meow_strf` 用**调用方自带缓冲**(而非静态/线程局部),避免 IME/模态等**重入路径**的缓冲竞态,且与 SDK 既有 `snprintf` 习惯一致(无格式参数时等价深拷贝 `meow_str`)。这是把任务书 `meow_strf(id, ...)` 落地为 `(out, outsz, id, ...)` 的定案(可重入、线程安全优先于少一个参数)。
### 13.2 取值优先级(四层,逐层回落,永不返回 NULL)
1. 当前 locale 的 **TF 覆盖表**:`MeowData/<pkg>/strings/<locale>.txt`,`id=text` 行格式 UTF-8,`meow_str_set_locale` 加载进静态数组(≤256 条);
2. 内嵌表**当前语言**(`en_us→en`,其余→`zh`);
3. 内嵌表**内置默认**(`zh_cn`,locale 缺省/未知语言回落);
4. 仍不可得 → **原样返回 `id`**(调用方可展示 id/打点,绝不见 NULL)。
> 无卡/无覆盖文件/无该 id → 自动回落**内嵌双语默认表**(编译进二进制,必可用)。内嵌表含 `common.*`(确定/取消/返回/确认/完成/保存/删除/编辑/搜索/输入/请输入/请选择/加载中/暂无数据/重试/失败/重试失败/关闭/未知/设置/点击确定编辑,共 21 条)+ `ime.*`(空格/确定/取消/拼英/?123/清/拼音/(无候选)/中,共 9 条),`zh_cn`+`en_us` 双语。
### 13.3 ID 规范
`域.名`(`common.ok` / `common.cancel` / `player.pause`)。SDK 自带 `common.*` 通用组 + `ime.*` IME 键面组;第三方可自建 `域`(如 `player.*`),经 TF 覆盖表追加,不改 SDK 二进制。
### 13.4 locale 存系统设置
locale 权威值 = 板载 `settings.conf` 的 `ui.locale`(默认 `zh_cn`)。SDK 首次调用**惰性读取**;`meow_str_set_locale` 即时切换(运行时)并 **best-effort 持久化**回 `ui.locale`(流式改写、保留其它行——绝不 O_TRUNC 整文件;失败静默,切换仍生效)。SDK 与 launcher 系统组件读同一份,后续系统 UI 复用同键即当前语言。
### 13.5 性能
**启动时一次加载**到静态线性表:内嵌为编译期静态表;覆盖表为启动/切 locale 时的静态数组(≤256 条)。`meow_str` 只做 O(n) 线性扫描(表小可接受),**热路径零 malloc**(无堆申请、无引用计数)。
### 13.6 SDK 控件接入
- **dialog**(§5.8.6):默认按钮 `"CANCEL"/"OK"` → `meow_str("common.cancel"/"common.ok")`;INPUT 预览占位 `"(tap OK to edit...)"` → `meow_str("common.tap_ok_edit")`。
- **IME**(§5.1):拼音键面/徽标(空格/确定/拼英/?123/清/拼音/(无候选))与 ASCII/NUM 顶条「中」→ `meow_str("ime.*")`。
- **空态/加载态**:SDK 不硬编码空态文案;`common.loading`/`common.no_data` 供 App 空态/加载中直接用(如 `meow_listview_set_empty_text(w, meow_str("common.no_data"))`)。
- **launcher 等系统组件**:后续经同框架接入(本任务只管 SDK 侧;launcher 复用同 `settings.conf ui.locale`)。
### 13.7 边界(明示不做,写清理由)
- **不做复数规则**:需占位符格式化协议 + 每语言复数表,超出「按 locale 取整串」的最小必要范围;
- **不做 BiDi / RTL / 动态字体回退**:需文本整形 + 布局镜像 + 字体回退策略,面向 480×720 单行/短文案场景暂不需要;
- 当前形态为 i18n **雏形**,复杂变体(复数/语言方向/字体重载)留后续批次;locale 仅 `zh_cn`/`en_us` 双语(任意串作覆盖表文件名,内嵌按非 `en` 视为内置默认)。
### 13.8 同步重编注意
`build_sdk.sh` OBJS 新增 `meow_ui_str`;`meow_ui_str.o`(含内嵌表)+ `meow_ui_dialog.o`/`meow_ui_ime.o` 重编 → **链 `meow_ui.a` 的 App 需重链**(一键 `build_sdk.sh`)。既有 App **不改源码**即零回归(仅按钮/键面文案随 locale 变化)。宿主回归测试需在编译命令补 `meow_ui_str.c`(各 test_*.c 头注释已同步)。
---
## 14. 遗留 / 待实现
1. `meow_ui` 渲染后端需从 launcher 里**抽公共绘制实现**(`back_buf`/`meow_dirty_*`/`meow_present`/`meow_font_bind` 目前是 launcher 静态函数——需提取为可链接的 `meow_ui_gfx` 或改为共享 obj,注意 launcher 与 app 是**不同进程**,不能共享同一 back_buf;`meow_ui.a` 是**独立库**,内部自建 fb 上下文,不复用 launcher 的全局 `g_dirty`)。
2. 输入线程优先级:vs 主循环 10ms 热键轮询的竞态,需在 B1 实现时定义清楚(应用进程内独立事件循环,不与 launcher 抢)。
+2 -2
View File
@@ -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
+15
View File
@@ -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 固定) */
+66 -11
View File
@@ -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();
}
+455
View File
@@ -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;
}
+187
View File
@@ -0,0 +1,187 @@
# SDK 时钟 ABI 说明(meow_ui_time_ms 真机故障的根因与修复)
面向对象:维护 SDK / 工具链 / 重链 App 的人。**结论先行**:这不是 SDK 代码写错,
是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾;修复落在 SDK 链上
(`patch_v2/sdk/meow_ui_time.c` + 链接期强定义),改后需重编 `meow_ui.a` 并重链四 App。
## 1. 现象(真机)
M10 抓到的日志首列(`meow_ui_time_ms()`,64 位)可拆成:
```
6607363222717682072 → lo=1135000 hi=1538396632
8754846870717682072 → lo=1135000 hi=2038396632
18138485921460348192 → lo=1140000 hi=4223195352
```
- 低 32 位 = 开机秒数 × 1000,**恒为 1000 的倍数 → 毫秒位丢失**;
- 高 32 位每次调用随机、无单调性。
同进程另一份取时(旧容器编的 core)正常 → 问题只在「zig 0.17 编的那部分」。
## 2. 根因链(三段证据)
### 2.1 zig 头文件:`time_t` 是 64 位,`struct timespec` 16 字节
`<zig lib>/libc/include/mips-linux-musl/bits/alltypes.h`:
```c
#define _REDIR_TIME64 1
#define __USE_TIME_BITS64 1
typedef _Int64 time_t; /* 无条件 64 位 */
struct timespec { time_t tv_sec; ...; long tv_nsec; ... }; /* tv_sec@0(8B)、tv_nsec@8 */
```
`include/generic-musl/time.h` 因此在 `_REDIR_TIME64` 下把 `clock_gettime` 重定向到
`__clock_gettime64`。用该链编译的 C 代码一律按「16 字节、tv_sec 8 字节」取数
(`patch_v2/sdk/meow_ui.a(meow_ui_gfx.o)` 的旧 `meow_ui_time_ms` 反汇编:`lw 0x10/0x14`
取 tv_sec、`lw 0x18` 取 tv_nsec,正是这个布局)。
### 2.2 zig 的 clang 给 mipsel 预定义了 `__ILP32__`
```
$ echo | zig cc --target=mipsel-linux-musleabi -dM -E - | grep __ILP32__
#define __ILP32__ 1
```
(arm-linux-musleabi 同样为 1;x86-linux-musl / aarch64 为 0。容器里的 gcc 对 mips o32
**不定义** `__ILP32__`。)
### 2.3 musl 的 time64 回退分支被这句 `!__ILP32__` 整体编译掉
`<zig lib>/libc/musl/src/time/clock_gettime.c`:
```c
#if defined(SYS_clock_gettime64) && !defined(__ILP32__)
... __syscall(SYS_clock_gettime64, clk, ts); /* 4403 */
... 失败(ENOSYS) 再 __syscall(SYS_clock_gettime, clk, ts32); /* 4263 + 宽度转换 */
#else
r = __syscall(SYS_clock_gettime, clk, ts); /* 直接把 8 字节 time32 写进调用者缓冲 */
#endif
```
这句 `!__ILP32__` 本意只排除 x32(`__ILP32__` 是 x32 的标记),但 clang 把它当成
「32 位数据模型」的通名。于是 zig 的 mipsel libc 走进 `#else`:
`__clock_gettime64` 实际执行 **SYS_clock_gettime(4263)**,只写 8 字节
`{ int32 tv_sec; int32 tv_nsec; }`。
**证据(反汇编,当前 zig 0.17 静态链接的探针)**:
```
0002576c <__clock_gettime64>:
257c8: addiu $2,$zero,0x10a7 ; 4263 = SYS_clock_gettime(time32)
257cc: syscall ; $5 直接是调用者的 16 字节 struct 指针,无 4403 尝试、无转换
```
同一符号在**旧容器产物** `patch_v2/ncm_app/ncm`(已入库,gcc 构建)里是:
```
115748: addiu $2,$zero,0x1133 ; 4403 = SYS_clock_gettime64
11574c: syscall
...
115774: addiu $2,$zero,0x10a7 ; ENOSYS 后回退 4263,写入本地 ts32
...
; 再把 ts32 升成 64 位写回调用者(tv_sec=tv_sec、tv_nsec=tv_nsec)
```
两者对比即根因:**头文件 ABI(64 位 time_t)与 libc 实现 ABI(32 位 time32)不一致**。
### 2.4 数值解释(为什么「低 32 = 秒×1000、高 32 随机」)
libc 把 `{sec, nsec}` 当两个 int32 写进 16 字节缓冲的前 8 字节;SDK 按 64 位读:
```
tv_sec = sec | (nsec << 32) // 低字正确、高字=纳秒
tv_nsec = 缓冲偏移 8 的未初始化值 // 毫秒项≈0
result = tv_sec*1000 + tv_nsec/1e6
= sec*1000 + (nsec*1000 mod 2^32)<<32 + 垃圾/1e6
```
- 低 32 位只剩 `sec*1000`(nsec 项左移 32 位不落到低字)→ 恒为 1000 的倍数;
- 高 32 位 = `nsec*1000 mod 2^32` → 每次随机、不单调;
- `tv_nsec` 来自越界读 → 通常是小值/0(真机上恰好 0,所以没有额外尾数)。
## 3. 修复(`patch_v2/sdk/meow_ui_time.c`)
1. **取数走内核 o32 ABI**:`meow_ui_time_ms()` 用
`syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &{int32 sec; int32 nsec;})`,
再 `meow_time_ms_from_parts(sec, nsec)` 升成 64 位毫秒。完全不依赖 libc 头声明的
`struct timespec` 宽度 —— 即使将来 zig 修好(或换回容器链),内核 time32 入口始终存在。
2. **链接期修 libc 入口**:同一 TU 强定义 `__clock_gettime` / `__clock_gettime64`
(musl 的 `time()`、`gettimeofday()`、`__timedwait_cp()`(pthread 定时等待)内部都调
`__clock_gettime`)。应用链接顺序是 `... meow_ui.a ... -lc`,归档先解析符号,libc 里那份
坏实现(及其 VDSO 前导)不会被拉入 —— 用 `nm` 验证链接产物里没有 `cgt_init/__vdsosym`,
且 `__time64`/`__timedwait_cp` 的 `jal` 指向本 TU 的实现。
3. **睡眠入口一并修正(同源缺陷)**:`__nanosleep_time64` / `__clock_nanosleep(_time64)`
先按 time32 转换再发 `SYS_nanosleep`/`SYS_clock_nanosleep`,`rem` 再扩回 64 位。
修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
SDK 里 `usleep(16000)`(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。
4. **去掉四处重复的 `now_ms()`**:`meow_ui_gfx.c` / `meow_ui_sys.c` / `meow_ui_input.c` /
`meow_cachegov.c` 统一调 `meow_ui_time_ms()`(cachegov 保留宿主测试注入桩)。
5. 构建脚本 `build_sdk.sh` 的 OBJS 增加 `meow_ui_time`;宿主单测 `test_meow_time.c`
固定换算边界与「单调且前进」;探针 `tools/clock_abi_probe.c` 用于宿主/真机一眼验收。
非 mips 目标(宿主单测、aarch64 宿主)走普通 `clock_gettime`,行为不变。
## 4. 验证
宿主:
```
cd patch_v2/sdk
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/meow_ui_time.o
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/meow_ui_time.o -o /tmp/test_meow_time
/tmp/test_meow_time # == PASS ==(换算边界 + 单调前进)
```
交叉:
```
bash patch_v2/sdk/build_sdk_host.sh --verify # 成员清单/架构自检 + 0 warning
llvm-objdump -d meow_ui.a(meow_ui_time.o) # meow_ui_time_ms: syscall 4263, lw 0x18 取 nsec、sra 扩展 tv_sec
```
链接期接管(用真实归档链一个探针,检查符号来源):
```
# 不应出现 libc 的 VDSO 前导(cgt_init/__vdsosym)→ libc 的取时实现未被拉入
strings -a clock_abi_probe_sdk | grep -c __vdso # 期望 0
llvm-nm clock_abi_probe_sdk | grep -E 'clock_gettime|nanosleep'
# __time64 / __timedwait_cp 的 jal 应指向本 TU 的 __clock_gettime;睡眠走 SYS_nanosleep(4166)/clock_nanosleep(4265)
```
探针 `tools/clock_abi_probe.c`(宿主 `gcc` 或 mipsel 交叉;加 `-DMEOW_PROBE_WITH_SDK`
并链 `meow_ui.a` 可同时验收 `meow_ui_time_ms`):把 `struct timespec` 预填 0xAA 后取时,
若偏移 8..11 仍是 0xAA 即说明 libc 只写了 8 字节(ABI 不一致),退出码 2。
真机(塔台批准后):日志首列应连续、出现非整千差;`meow_ui_time_ms()` 两次取样差 = 真实毫秒。
本次设备已离线,真机确认**待办**。
## 5. 影响面
- **SDK 内**:`meow_ui_time_ms()` 及其上层 —— 动画 tween(`meow_ui_anim.c`)、滚动/fling
(`meow_ui_scroll.c`/`meow_select.c`)、IME 冷却(`meow_ui_ime.c`)、状态栏动画
(`meow_ui_gfx.c`)、通知 TTL(`meow_ui_notify.c`)、输入层 tick/启动冷却
(`meow_ui_input.c`)、cachegov 回退窗口(`meow_cachegov.c`)、日志首列(`meow_ui_log.c`)。
- **App 侧**:`on_tick` 入参 `now`、`meow_anim_*`、以及任何自行调用 `meow_ui_time_ms()` 的
App(hello/reader/files/ncm…)。
- **产物**:`meow_ui.a` 变更 → 需**重编并重链四 App**(ncm/reader/files/hello);
`time()`/`gettimeofday()` 的修正也在同一份归档里生效(链接期覆盖)。
- 真机写入红线未触碰;本单只改 SDK 源与构建脚本。
## 6. 同源残余(未在本单修复,需单独安排)
同一句 `!__ILP32__` 还打穿了以下路径(睡眠族已在上文一并修掉,余下这些症状不同、
修复面更大):
| 路径 | 症状 | 说明 |
| --- | --- | --- |
| `futex` 相对超时 | 亚秒 timed wait 立即超时 | `__timedwait_cp` 的 `__futex4_cp` 同款 `!__ILP32__` 守卫;`pthread_cond_timedwait` 的「now」已由本单修正,但相对超时仍按 8 字节读(>1s 的整秒等待基本可用,亚秒等待失效) |
| `timerfd_settime` | 到期时间错乱 | `struct itimerspec`(32 字节)当 16 字节读;当前 SDK 未用 timerfd |
| `clock_settime`/`clock_adjtime`/`pselect`/`ppoll`/`sigtimedwait`/`semtimedop` 等 | 未验证 | 同族守卫;SDK 目前未直接使用 |
建议:向 zig 上游报 musl `__ILP32__` 守卫问题(clang 对 32 位目标统一定义 `__ILP32__`,
而该守卫本意只排除 x32);根治应换用容器旧链(gcc,不定义 `__ILP32__`)编 SDK,或
downgrade/修补 zig 内置 musl。**不要**指望 `-D_TIME_BITS=32`:zig 的
`alltypes.h` 是无条件 `#define __USE_TIME_BITS64 1`,命令行改不动;而只改用户侧头文件会让
用户结构(8 字节)与 libc 内部(16 字节,pthread 等)再次错配。
+59
View File
@@ -0,0 +1,59 @@
# sdk/ — meow_ui 应用框架(源码 + 构建)
本目录是 `meow_ui.a` 的**自包含**源码树:公共头在 `include/`(伞头 `meow_ui.h`),
构建脚本 `build_sdk.sh`,宿主逻辑测试 `test_*.c`。上手指南见仓库根
[README.md](../README.md) 与 [docs/APP_DEV_GUIDE.md](../docs/APP_DEV_GUIDE.md)。
## 构建
```sh
cd sdk
./build_sdk.sh # 产出 sdk/meow_ui.a(mipsel 静态库)
```
交叉编译器默认 `$HOME/Project/R3ProII/.tools/zig-mipsel-cc`,可用 `MEOW_CC` 覆盖。
## 时钟:mipsel o32 时钟/睡眠 ABI 修复(已内置)
**公开 SDK 现已内置 mipsel o32 时钟/睡眠 ABI 修复(见 [CLOCK_ABI.md](CLOCK_ABI.md))**,
源码在 `meow_ui_time.c`。
- 背景:zig 内置 musl 在 mipsel 上「头文件声明 64 位 `time_t`、实现却走 time32 系统调用」,
导致 `meow_ui_time_ms()` 毫秒位恒 0、高 32 位随机;`usleep()` 亚秒等待立即返回(忙等空转)。
- 修复:`meow_ui_time_ms()` 直接按**内核 o32 ABI**(8 字节 `{int32 sec; int32 nsec;}`)取时,
再自行升成 64 位毫秒;同一 TU 强定义 `__clock_gettime` / `__clock_gettime64` /
`__clock_nanosleep` / `__clock_nanosleep_time64` / `__nanosleep_time64`,在**链接期接管**
libc 里那份 ABI 不一致的实现。
- **链接顺序要求**:应用必须把 `meow_ui.a` 放在 `-lc` **之前**(`cc ... meow_ui.a ... -lc`),
强定义才能生效。仓库内 `examples/hello/build_hello.sh` 与 `tuku/build_tuku.sh` 均如此;
自定义构建脚本请照抄这个顺序,否则取时/睡眠会静默回到坏实现。
- 非 mipsel 目标(宿主单测、aarch64 等)走普通 `clock_gettime`,行为不变。
### 已知限制
- **2038 前有效**:内核侧仍是 time32(`tv_sec` 为 int32),秒数超出 `INT32_MAX` 即溢出;
本修复只保证「头/实现的宽度一致」,不把内核升到 time64。
- 同源缺陷**未修**:futex 相对超时(`pthread_cond_timedwait` 的亚秒等待)、`timerfd_settime`、
`clock_settime`/`clock_adjtime`、`pselect`/`ppoll` 等仍受同一句 `!__ILP32__` 影响;
清单与影响见 [CLOCK_ABI.md](CLOCK_ABI.md) §6。SDK 自身不直接使用这些路径。
- 真机验收:本修复的证据是反汇编 + 宿主单测(本机无 qemu-mipsel),设备侧仍需实测确认。
## 宿主测试
无硬件即可跑(`gcc` 直接编译,不链 `meow_ui.a`):
```sh
cd sdk
# 时钟换算与单调性
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time
./test_meow_time # == PASS == / checks=N
# 伞头与其余模块测试(示例)
gcc -I. -Iinclude test_umbrella.c <各 sdk 源 .c> test_host_stub.c -lpthread -o test_umbrella
./test_umbrella
```
注意:宿主编译时 `meow_ui_time.c` 走 `MEOW_TIME32_KERNEL_ABI 0` 分支(普通 `clock_gettime`),
因此**宿主测试不能证明 mipsel o32 路径正确** —— 那条路径的正确性靠 `CLOCK_ABI.md §4` 的
反汇编证据(`nm` 符号来源 + `syscall 4263/4265` 的 8 字节转换)。
+10 -2
View File
@@ -6,6 +6,11 @@
#
# 交叉编译器:默认 MeowOS 开发机路径 $HOME/Project/R3ProII/.tools/zig-mipsel-cc;
# 可用环境变量 MEOW_CC 覆盖(或用你自己装好的 zig 交叉 cc,--target=mipsel-linux-musleabi soft-float)。
#
# 时钟(meow_ui_time):mipsel 上 zig 内置 musl 的 time64 ABI 自相矛盾(头声明 64 位 time_t、
# 实现走 time32 取数),该 TU 按内核 o32 ABI 取时并**强定义** __clock_gettime*/睡眠入口,靠链接期
# 接管 libc 那份坏实现。因此应用链接顺序必须是 `... meow_ui.a ... -lc`(归档在 libc 之前,
# 现有 build_tuku.sh 即如此);根因与证据链见 sdk/CLOCK_ABI.md。
set -e
cd "$(dirname "$0")"
CC="${MEOW_CC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}"
@@ -14,8 +19,11 @@ AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架
rm -f meow_ui.a *.o
# meow_font.c = 预渲染位图字体运行时(自包含:只含 meow_font.h + 标准头;最小 zlib ABI 自声明,
# 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。
OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font"
# 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。新模块(box/anim/scroll/str + cachegov/lazygrid/
# select/net_ipc)随主仓增量并入。
# meow_ui_time.c 必须**编进归档**且排在前面:它提供 meow_ui_time_ms(gfx 已不再实现)与
# __clock_gettime* 强定义,归档成员随 meow_ui_time_ms 的引用被拉入,从而在 -lc 之前解析。
OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_jpeg meow_png_stream meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc"
CFLAGS="-Os -static -Iinclude -I."
for f in $OBJS; do
echo "== cc $f =="
+89
View File
@@ -0,0 +1,89 @@
/* demo_box_scroll.c — 宿主离屏 demo:§12.1 meow_box 布局 + §12.3 meow_scroll fling 端到端。
* 渲染一屏「标题(固定) / 列表(weight 撑满) / 底栏(固定)」的 box 布局(cross 拉伸),
* 再对列表跑一次 fling(速度跟踪→惯性滑动到停止处),并 dump PPM 供目检。
*
* 编译(宿主 gcc,链真实 SDK 源 + 打桩 font/global):
* gcc -o demo_box_scroll demo_box_scroll.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:MEOW_UI_DUMP=/tmp/demo_box.ppm ./demo_box_scroll
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define NROW 60
#define ITEM 44
static int src(int idx, char *label, int len, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= NROW) return 0;
snprintf(label, (size_t)len, "item %d", idx);
return 1;
}
int main(void)
{
/* 离屏 dump */
setenv("MEOW_UI_DUMP", "/tmp/demo_box.ppm", 1);
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("DEMO FAIL: init\n"); return 1; }
const meow_theme *th = meow_theme_get();
meow_ui_clear(th->bg);
/* ---- box 布局:标题(固定) / 列表(weight) / 底栏(固定),交叉轴拉伸 ---- */
meow_box *bx = meow_box_new(MEOW_BOX_V);
meow_box_set_rect(bx, 0, 0, 480, 720);
meow_box_set_padding(bx, 12, 12, 12, 12);
meow_box_set_gap(bx, 8);
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_STRETCH);
meow_widget *header = meow_btn_create(0, 0, 480, 44, "Header (fixed)");
meow_widget *list = meow_list_create(0, 0, 480, 100, NROW, src, NULL);
meow_list_set_item_h(list, ITEM);
meow_widget *footer = meow_btn_create(0, 0, 480, 52, "Footer (fixed)");
meow_box_add(bx, header, MEOW_BOX_FIXED, 0);
meow_box_add(bx, list, MEOW_BOX_WEIGHT, 1);
meow_box_add(bx, footer, MEOW_BOX_FIXED, 0);
meow_box_arrange(bx);
int hr[4], lr[4], fr[4];
meow_widget_rect(header, hr);
meow_widget_rect(list, lr);
meow_widget_rect(footer, fr);
printf("DEMO layout: header(%d,%d,%d,%d) list(%d,%d,%d,%d) footer(%d,%d,%d,%d)\n",
hr[0], hr[1], hr[2], hr[3], lr[0], lr[1], lr[2], lr[3], fr[0], fr[1], fr[2], fr[3]);
meow_widget_draw(header);
meow_widget_draw(list);
meow_widget_draw(footer);
meow_ui_flush(); /* 上屏并 MEOW_UI_DUMP 写 PPM *//* (host 离屏无 fb → 只落盘) */
/* ---- fling:对列表做一次惯性下滑(速度跟踪→减速→停)---- */
meow_scroll *s = meow_scroll_wrap(list, 0);
int maxs = meow_list_max_scroll(list);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 14; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) { meow_anim_tick(); usleep(10000); }
meow_anim_tick();
int end = meow_scroll_offset(s);
printf("DEMO fling: max_scroll=%d final=%d (active=%d)\n", maxs, end, meow_scroll_active(s));
meow_scroll_free(s);
meow_box_free(bx);
meow_widget_free(list);
meow_widget_free(header);
meow_widget_free(footer);
meow_ui_shutdown();
printf("DEMO OK\n");
return 0;
}
+82
View File
@@ -0,0 +1,82 @@
/*
* meow_cachegov.h — 磁盘缓存 page-cache 治理 SDK 库(A 的主导/治本侧)。
*
* 归属(devdocs/plan/ARCHITECTURE_PRINCIPLES.md ③混合,2026-09-02 用户定案):
* - A(DONTNEED 热路径,需要 read_pos) → 随 SDK 库(本文件,进程内直读 read_pos,无 IPC)。
* - C(设备级 /proc/meminfo Cached 监控降载)→ 归系统服务 meow_svc_perf;本模块只读其共享状态
* 文件(MEOW_CACHEGOV_C_FILE)作为「降载」信号,供 App 查询。
* - App(如 ncm)侧只调本库接口:登记文件 + read_pos 回调 + tick + drop_finished + 查询降载,
* 不写自己的治理算法。
*
* 权威方案:devdocs/plan/OOM_PAGECACHE_GOVERNANCE.md。红线:**L2(丢 read_pos 前方)禁止**,
* 本库绝不 drop `[read_pos, ...)` 任何未解码区。
*
* 分级(§2.2):
* L0(默认,安全): 曲终/commit 后对整文件 posix_fadvise(DONTNEED)——播放器已退出解码循环。
* L1(可选,主杠杆): 播放中丢已读前缀 `[0, read_pos-keep_hot)`,保 keep_hot 窗口供倒带;
* 仅 drop_mode>=1 且 A 未回退到 ≥2 时执行。
* L2(禁止,红线): 绝不丢 read_pos 前方/当前未解码页。
*
* underrun 回退(§2.3,SDK 内置):App 在 pread 慢(数据在盘上但 >threshold ms)处上报
* meow_cachegov_note_refault();本库滚窗 10s,超门限逐级回退(放大 keep_hot → 降 L0 → 关 A)。
*
* 接口按 OOM 文档 §4.2 + §4.4;值语义与 mem_watch 的「滞回三档」风格对齐(自含:pthread + meow_log)。
*/
#ifndef MEOW_CACHEGOV_H
#define MEOW_CACHEGOV_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct meow_cachegov_cfg {
int enabled; /* 总开关:0=A 完全不丢页(register/tick/drop_finished 全部 no-op) */
int cached_threshold_kb; /* 保留:C 侧阈值归 svc_perf(本库恒 0,只读不控,防误用) */
int dirty_warn_kb; /* 保留:同上(svc_perf 用;本库只透传日志) */
size_t keep_hot_bytes; /* L1 保留窗口:read_pos 之后保多少不丢(回退时自动放大) */
size_t min_hot_ahead; /* L2 红线:read_pos 前方至少保多少——仅防御注释,**绝不参与 drop** */
int drop_mode; /* 0=仅曲终(L0);1=曲终+播放中丢已读前缀(L0+L1,配 seek 回退) */
int refault_backoff; /* 10s 窗口内 refault 触发回退的门限(次数) */
} meow_cachegov_cfg_t;
/* 登记本次治理的缓存文件(正式缓存文件路径 + 大小 + read_pos 回调 + 回调 ctx)。
* 返回 0=ok / 负=错。重复 path 幂等(替换条目)。 */
int meow_cachegov_register(const char *path, uint64_t size,
uint64_t (*read_pos_fn)(void *ctx), void *ctx);
/* 注销该 path 的治理条目(曲终/切歌后清理)。返回 0=找到并删 / 负=不在册。 */
int meow_cachegov_unregister(const char *path);
/* 周期调用(播放循环节拍 / 监控线程均可):
* ① 按回退档执行 L1 丢弃(丢已读前缀 [0, read_pos-keep_hot));
* ② 评估 refault 滚窗做逐级回退。 */
int meow_cachegov_tick(void);
/* 曲终/commit 后:对刚播完文件做整文件 L0 DONTNEED + 注销该条目。
* 回退档>=3(A 整体关闭)时不丢页(L0 也被关,见 §2.3 步骤 3)。 */
int meow_cachegov_drop_finished(const char *path);
/* 下载/预取方查询(1=应暂停)。数据源 = A 侧回退硬暂停 OR svc_perf C(Cached 超阈值)降载。 */
int meow_cachegov_download_paused(void);
int meow_cachegov_prefetch_paused(void);
/* 设置/读取治理配置(运行时覆盖编译期默认)。 */
int meow_cachegov_set_cfg(const meow_cachegov_cfg_t *cfg);
int meow_cachegov_get_cfg(meow_cachegov_cfg_t *cfg);
/* ---- refault 上报 + A 回退状态(underrun 检测,§2.3)---- */
/* App 在 pread 慢(数据已在盘上、读出 >threshold ms)处上报一次 refault。线程安全。 */
void meow_cachegov_note_refault(void);
/* A 当前回退档:0=正常 1=keep_hot 放大 2=降 L0(L1 关) 3=A 关。 */
int meow_cachegov_backoff_level(void);
/* ---- 宿主测试桩(设备永不调用;-1=真实时钟,>=0 时用注入值替代 monotonic 时钟)---- */
void meow_cachegov_test_set_time_ms(int64_t t);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_CACHEGOV_H */
+125 -8
View File
@@ -35,6 +35,12 @@
/* 列表编码上限(count 个 NUL 字符串时,distinct 工作缓冲上限) */
#define MEOW_IPC_CARGO 1024u /* 守护进程入队拷贝上限(可小于 MEOW_IPC_MAX) */
/* ---- page-cache 治理(OOM_PAGECACHE_GOVERNANCE.md,③混合)----
* meow_svc_perf(C:/proc/meminfo Cached 监控降载)把「是否暂停下载/预取」写入此共享状态文件
* (一行式 key=value;App/SDK meow_cachegov 只读,系统级单点,不重复起线程)。
* 路径可被 env MEOW_CACHEGOV_C_FILE 覆盖(宿主测试隔离)。 */
#define MEOW_CACHEGOV_C_FILE "/tmp/meow_cachegov_c.state"
/* ---- 消息类型(FRAMEWORK_DESIGN §1 表)---- */
enum {
MSG_NONE = 0x0000,
@@ -75,6 +81,7 @@ enum {
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
@@ -93,6 +100,10 @@ enum {
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */
MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */
/* P2 蓝牙(BLUETOOTH_FULL_OVERHAUL_PLAN §五):输出目标切换。audiod 读 settings.conf
* `audio.output_bt`(0/1) 或收本消息改写目标(0=OUT_DAC 1=OUT_BT),重配 PCM。
* 与物理路由 numid=9 解耦:蓝牙非物理端口,不写 numid=9、不触发 FPGA 烧录。 */
MSG_AUDIO_OUTPUT = 0x00CB, /* ↑ 输出目标(u32 target:0=DAC 1=BT;ui/CLI 下发) */
/* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ----
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
* 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power:
@@ -268,6 +279,29 @@ typedef struct m_playctl {
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)----
* App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页
* 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限,
* 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。
* - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用)
* - kind = 0 原文 lrc / 1 翻译 tlrc
* - total= 该 kind 总字节数(不含 NUL;0=清空该 kind)
* - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送)
* 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲,
* 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */
#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */
#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */
#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */
typedef struct m_lyric {
char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */
uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */
uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */
uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */
uint16_t off; /* 本片起始偏移(≤ total;首片=0) */
uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */
char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */
} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
@@ -451,24 +485,32 @@ typedef struct m_picker_result {
/* ================= audiod 独占音频服务(AUDIOD_DESIGN.md) = */
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE。
* v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32 处理;FLOAT v1 拒绝。 */
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE
* 5=AUDIO_FMT_DOP(DoP DSD 透明容器格式)。v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32
* 处理;FLOAT v1 拒绝。AUDIO_FMT_DOP 为 **独立格式值**(A3+DSD 定案:避免 S24 名义(3B)/实际(4B)歧义),
* 仅在 MSG_AUDIO_OPEN.flags 带 AUDIO_FLAG_DSD 时合法;audiod 只做 DoP 容器透传(不引 dsd2pcm)。 */
enum {
AUDIO_FMT_S16 = 1,
AUDIO_FMT_S24 = 2,
AUDIO_FMT_S32 = 3,
AUDIO_FMT_FLOAT = 4
AUDIO_FMT_FLOAT = 4,
AUDIO_FMT_DOP = 5 /* DoP DSD(32-bit 容器:bits[31:16]=16bit DSD 数据,bits[15:8]=标记 0xFA/0x05,bits[7:0]=0),
容器率 = DSD 位率/16(DSD64→176400 / DSD128→352800 / DSD256→705600) */
};
/* MSG_AUDIO_OPEN.flags 位(预留曾恒 0;A3+DSD 起使用) */
#define AUDIO_FLAG_DSD 0x00000001u /* fmt==AUDIO_FMT_DOP:本流为 DoP DSD(App 侧已解码),audiod 只透传 */
/* MSG_AUDIO_OPEN ↑:请求开流(App 原生声源参数)。grant_*(ACK)回 audiod 实际授权给客户端的
* 产出格式(率/声道/位深)——客户端必须按 grant 格式产出 PCM(SDK meow_ui_audio 负责归一:
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。 */
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。fmt==AUDIO_FMT_DOP
* 须带 AUDIO_FLAG_DSD(否则 ERR_INVAL);DSD 经 DoP 透传,audiod 不引 dsd2pcm。 */
typedef struct m_audio_open {
char app_id[MEOW_APP_ID_MAX]; /* 32 */
uint32_t rate; /* 原生采样率(应用源,如 44100/48000) */
uint32_t rate; /* 原生采样率(应用源,如 44100/48000;DSD 容器率) */
uint16_t ch; /* 原生声道数(1=mono 2=stereo) */
uint16_t fmt; /* 原生格式(AUDIO_FMT_*) */
uint32_t flags; /* 预留(0) */
uint16_t fmt; /* 原生格式(AUDIO_FMT_*;DSD 用 AUDIO_FMT_DOP) */
uint32_t flags; /* AUDIO_FLAG_DSD 等 */
} m_audio_open_t; /* 32+4+2+2+4 = 44 B */
/* MSG_AUDIO_OPEN_ACK ↓:授权结果。err=MEOW_OK 才有效(stream_id>0 且 grant_* 为实际授权值);
@@ -499,11 +541,24 @@ typedef struct m_audio_route {
} m_audio_route_t; /* 4 B */
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
* + 持久化 settings.conf `audio.volume`,与 watchdog/launcher 同源同键)。 */
* + **DSD_VOLUME(音量收编 v2 泛化双组写,保证 DSD↔PCM 切换连续/防爆音)** + 持久化 settings.conf
* `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时**不下发 DAC 音量反转**
* (音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
typedef struct m_audio_volume {
int32_t pct; /* -1=查询;0-100=设置 */
} m_audio_volume_t; /* 4 B */
/* 输出目标枚举(msg AUDIO_OUTPUT / settings.conf `audio.output_bt` 共用;亦用于 CLI output) */
enum {
AUDIO_OUT_DAC = 0, /* 本地 DAC(hw:0,0,独占/period=1024×4/FPGA numid=9/音量反转) */
AUDIO_OUT_BT = 1 /* 蓝牙 A2DP source(pcm.bluealsa 播放向,透传+格式归一,AVRCP 控音量) */
};
/* MSG_AUDIO_OUTPUT ↑:切换输出目标(ui/CLI 下发)。0=DAC 1=BT。audiod 侧重配 PCM。 */
typedef struct m_audio_output {
uint32_t target; /* AUDIO_OUT_DAC / AUDIO_OUT_BT */
} m_audio_output_t; /* 4 B */
/* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)=====
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
@@ -601,4 +656,66 @@ typedef struct m_net_dns_result {
char text[MEOW_IPC_MAX - 8];
} m_net_dns_result_t;
/* ---- 期 3:管控层(NETMGR_DESIGN §6;默认 allow,按实体手动 block + 域名白/黑)----
* 0x00EA MSG_NET_POLICY(同时承载 GET/SET,op 区分)。GET=读级任意;SET=写级仅系统身份
* (SO_PEERCRED → /usr/bin/meow_* / os.meow.* / MeowPanel)。防自锁实体(dropbear/wpa_supplicant/
* udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init)拒绝置 block(服务端硬校验)。
* 策略持久化:`/data/meow/netmgrd/net_policy.conf`(/data=/usr/data 符号链接),读改写单键原子写。 */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_POLICY = 0x00EA, /* ↑ op=0 查询 / op=1 写(set net / set domain / del) */
MSG_NET_POLICY_RESULT = 0x00EB /* ↓ 策略文本/结果 */
/* 0x00EE 事件推送(预留,期2 订阅制)/ 0xEF 余量(均为预留) */
};
/* MSG_NET_POLICY 操作码 */
enum {
NET_POLICY_SHOW = 0, /* show [entity]:entity 空=全部 */
NET_POLICY_SET_NET = 1, /* set <entity> allow|block */
NET_POLICY_SET_DOMAIN = 2, /* domain <entity> +host|-host */
NET_POLICY_DEL = 3 /* del <entity> */
};
typedef struct m_net_policy {
uint32_t op; /* NET_POLICY_* */
char entity[48]; /* app:<pkg> | svc:<svc> | proc:<comm> | global;空=适用 SHOW 的全量 */
char arg[64]; /* SET_NET:allow|block;SET_DOMAIN:+host|-host;其余空 */
} m_net_policy_t; /* 120 B */
typedef struct m_net_policy_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / (防自锁)MEOW_ERR_PERM */
uint16_t text_len;
uint16_t pad;
char text[MEOW_IPC_MAX - 8];
} m_net_policy_result_t;
/* ---- 期 4:SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;0xEE/0xEF)----
* 一、meow_net_policy_check(host):框架 App 联网前自查 host 是否被 netmgrd 策略放行——
* 服务端以调用方 app_id(msg.app_id)→ 实体 app:<pkg>,评估 policy_net_blocked(entity)
* || dns_query_domain_denied(entity, host),放行=allow 1 / 拒绝=block 0;netmgrd 不在 → SDK 侧放行。
* 二、meow_net_report(rx,tx):App 自愿上报本进程收发字节数(内核无 per-socket/per-uid 计数,
* §4 诚实声明的「字节级记账走 SDK 自报」);netmgrd 记入 per-实体统计(status JSON entity_bytes)。
* 读级/上报任意调用者(自愿自报,无副作用;block 判定由服务端策略引擎权威执行)。
* (0x00EE 曾留作「事件推送 订阅制」,期 4 起改作 SDK 通道;事件推送期 2 从未实装。) */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_SDK = 0x00EE, /* ↑ op=0 策略检查 / op=1 字节上报 */
MSG_NET_SDK_RESULT = 0x00EF /* ↓ code + (CHECK)allow + 状态文本 */
};
enum { NET_SDK_CHECK = 0, NET_SDK_REPORT = 1 };
typedef struct m_net_sdk {
uint32_t op; /* NET_SDK_CHECK / NET_SDK_REPORT */
char app_id[MEOW_APP_ID_MAX]; /* 调用方 application(SDK 用 env MEOW_LOG_APP;CLI/测试可任意) */
char host[64]; /* CHECK:待查域名 */
uint32_t rx; /* REPORT:本进程累计 RX 字节 */
uint32_t tx; /* REPORT:本进程累计 TX 字节 */
} m_net_sdk_t; /* 4+32+64+4+4 = 108 B */
typedef struct m_net_sdk_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_* */
int32_t allow; /* CHECK:1=allow 0=block(block 由服务端策略引擎判定) */
uint16_t text_len;
uint16_t pad;
char text[MEOW_IPC_MAX - 12];
} m_net_sdk_result_t;
#endif /* MEOW_IPC_H */
+106
View File
@@ -0,0 +1,106 @@
/*
* meow_lazygrid.h — SDK 2 维虚拟化宫格(§12.4:meow_lazygrid,M7.4 主界面前置)。
*
* 定位:给「卡片/图标宫格」提供**二维虚拟化**——应用入口宫格、图标墙、设置快捷卡等。
* 只实例化**可见行**(视口内),行高/列几何 + gap;滚动复用 §12.3 meow_scroll(fling/snap),
* 与 meow_listview 同款惯性;命中 meow_lazygrid_hit/tap_at 把滚动+网格坐标映射回 item idx。
*
* 设计(仿 meow_listview 的 vsource 思想 + meow_scroll 滚动 + meow_box 列几何):
* - 数据源:count_fn() 返回总项数(任意长),bind_fn(index, item) 仅当 item「进入视口」
* (绑定到固定池槽)时调一次;回滚命中同槽不重绑(同 listview 池语义)。
* - 固定 item 池:池容量 = 视口行数 + 2(× cols),静态有界(≤MEOW_LAZYGRID_POOL_MAX)。
* - 列几何:cols + cell_w + cell_h + gap;行高 row_h = cell_h + gap;按 scroll offset 推可见行。
* - 渲染:默认 renderer 画「圆角卡片(icon+名+状态)」;App 也可经 set_item_draw 完全自绘
* (主界面卡片宫格用:icon+名+后台标注)。支持选中高亮/禁用变淡/空态占位。
* - 滚动:接 meow_scroll(meow_lazygrid_scroll 返回会话;track/fling/snap 与 listview 同款)。
*
* 内存纪律(M7 §5.2):静态有界 item 池(冷路径 create/set_vsource 分配,热路径 draw/hit/scroll
* 零 malloc),与 listview 池同源;不常驻 count 个 item 对象。
*
* 非 meow_widget:lazygrid 是**自包含控件**(不注册进 z 序/绘制注册表),由宿主(launcher session)
* 在 draw 阶段调 meow_lazygrid_draw、命中阶段调 meow_lazygrid_hit/tap_at、滚动喂 meow_scroll——
* 与 meow_scroll/meow_box 同为「正交模块」,不侵入 struct meow_widget(全追加式)。
*/
#ifndef MEOW_UI_LAZYGRID_H
#define MEOW_UI_LAZYGRID_H
#include "meow_ui_types.h" /* meow_img 前向 + 基础类型(子头不引伞头,避免相对路径依赖) */
#include "meow_ui_theme.h" /* meow_theme(draw 取语义色) */
#include "meow_ui_scroll.h" /* meow_scroll(滚动集成;子头引依赖子头,同 widgets.h→theme.h) */
#ifdef __cplusplus
extern "C" {
#endif
typedef struct meow_lazygrid meow_lazygrid;
/* ---- cell / item ---- */
/* 单格数据(bind 回调填充;SDK 固定池持有;App 跨 bind 用 user 存业务态/图/index)。 */
typedef struct meow_lazygrid_item {
const char *title; /* 主文本(名;必填,NULL 视为空格) */
const char *subtitle; /* 副文本(状态/后台标注),可空 */
const meow_img *icon; /* 可选:格内图标(NULL=无) */
int enabled; /* 1=可交互;0=变淡不响应 */
void *user; /* App 私有(跨 bind 保存业务态;SDK 不碰) */
} meow_lazygrid_item;
/* 数据源:count 返回总项数(>=0,任意长);bind 在 item 进入视口时调一次填充 item。
* 二者均不常驻 item 对象;count 每帧实时查(支持动态长度/搜索过滤)。 */
typedef int (*meow_lazygrid_count_fn)(void *ctx);
typedef void (*meow_lazygrid_bind_fn)(int index, meow_lazygrid_item *item, void *ctx);
/* 完全自绘回调(覆盖默认 renderer):index=项号;x,y,w,h=格矩形(已裁剪到宫格视口);
* state=MEOW_LAZYGRID_ST_* 状态位(选中/禁用组合)。App 用 §3 绘制 API 自绘整格。 */
typedef void (*meow_lazygrid_draw_fn)(int index, int x, int y, int w, int h,
int state, void *ctx);
/* state 位(draw 回调收到)。 */
#define MEOW_LAZYGRID_ST_SELECTED (1u<<0)
#define MEOW_LAZYGRID_ST_DISABLED (1u<<1)
/* 命中/点击回调(idx=项号;App 在 tap_at 命中后触发动作)。 */
typedef void (*meow_lazygrid_tap_fn)(int idx, void *ctx);
#define MEOW_LAZYGRID_POOL_MAX 96 /* 池容量上限(固定;保证静态有界) */
/* ---- 生命周期(冷路径 create/free;热路径 draw/hit/scroll 零 malloc)----
* cols=列数(>=1,越界 clamp);cell_w/cell_h=格宽高;gap=格间距(水平与垂直同用)。 */
meow_lazygrid *meow_lazygrid_create(int x, int y, int w, int h, int cols,
int cell_w, int cell_h, int gap);
void meow_lazygrid_free(meow_lazygrid *g);
/* ---- 几何配置 ---- */
void meow_lazygrid_set_rect(meow_lazygrid *g, int x, int y, int w, int h); /* 视口矩形(绝对坐标) */
void meow_lazygrid_set_cols(meow_lazygrid *g, int cols);
void meow_lazygrid_set_cell(meow_lazygrid *g, int cell_w, int cell_h, int gap);
/* ---- 数据源 + 渲染 ---- */
int meow_lazygrid_set_vsource(meow_lazygrid *g, meow_lazygrid_count_fn count,
meow_lazygrid_bind_fn bind, void *ctx);
int meow_lazygrid_set_item_draw(meow_lazygrid *g, meow_lazygrid_draw_fn draw,
void *ctx); /* NULL=用默认卡片 renderer */
int meow_lazygrid_set_empty_text(meow_lazygrid *g, const char *text);
/* ---- 滚动(像素级;可视行数按 offset 实时推) ---- */
void meow_lazygrid_set_scroll(meow_lazygrid *g, int off);
void meow_lazygrid_scroll_by(meow_lazygrid *g, int dy);
int meow_lazygrid_scroll(meow_lazygrid *g);
int meow_lazygrid_max_scroll(meow_lazygrid *g); /* max(0, 内容高 - 视口高) */
int meow_lazygrid_row_h(const meow_lazygrid *g); /* 行高(=cell_h+gap) */
/* 便捷:包一个 meow_scroll 会话(set/max/get/redraw 指回本宫格;redraw 只刷视口区)。
* App 在 on_touch_move 喂 meow_scroll_track、松手 meow_scroll_fling。返回 NULL=池满。 */
meow_scroll *meow_lazygrid_scroll_attach(meow_lazygrid *g);
/* ---- 选中 / 命中 ---- */
void meow_lazygrid_set_on_tap(meow_lazygrid *g, meow_lazygrid_tap_fn cb, void *ctx);
void meow_lazygrid_set_sel(meow_lazygrid *g, int idx); /* 越界 clamp;<0 清除;标脏 */
int meow_lazygrid_sel(const meow_lazygrid *g);
int meow_lazygrid_hit(meow_lazygrid *g, int x, int y); /* 命中项 idx(滚动+网格坐标映射);-1=未命中/禁用 */
int meow_lazygrid_tap_at(meow_lazygrid *g, int x, int y); /* 命中:置选中 + 回调 + 返回 idx/-1 */
/* ---- 绘制 ---- */
void meow_lazygrid_draw(meow_lazygrid *g, const meow_theme *th);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_LAZYGRID_H */
+47
View File
@@ -0,0 +1,47 @@
/*
* meow_select.h — SDK 枚举/预设选择器(§5.8.6 模态组件,M7.2 设置页缺口 #4)。
*
* 定位:给「枚举 / 预设选择器」一个开箱即用的**下拉模态**——设置页采样率/码率/主题/语言等
* 枚举、均衡器预设等。封装 = BottomSheet(标题 + 选项列表)+ meow_listview(SINGLE) 单选列表:
* 选项数组 + 当前选中(高亮)+ 点选项 = 选中 + 关闭 + on_change 回调;点面板外/左缘返回 = 取消。
*
* 设计(§3 缺口 #4):选项用 meow_listview(SINGLE) 渲染(单选高亮、可惯性滚动、虚拟化长列表),
* 外层是模态 BottomSheet(复用 meow_dialog 的遮罩/快照恢复,scrim 只叠一层/关闭无残影)。
* 阻塞式(内层 meow_input_pop 轮询,与 meow_dialog 同构);打开前排空残留触摸。
*
* 内存纪律:选项数组定长(≤MEOW_SELECT_MAX_ITEMS,栈/数组持有,非 malloc 长列表);
* 列表在 open 时创建(冷路径)、close 时释放;open 循环零 malloc。
*/
#ifndef MEOW_UI_SELECT_H
#define MEOW_UI_SELECT_H
#include "meow_ui_types.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct meow_select meow_select;
/* 选中回调(on_change):idx=被选中的选项下标(0..n-1)。 */
typedef void (*meow_select_fn)(int idx, void *ctx);
#define MEOW_SELECT_MAX_ITEMS 32 /* 选项上限(静态数组;越界 add 忽略并返回 -1) */
/* ---- 组件式(可复用 + on_change 回调)---- */
meow_select *meow_select_create(const char *title, int cur); /* cur=初始选中项;越界 clamp */
int meow_select_add_item(meow_select *s, const char *label); /* 返回项下标;满/无效= -1 */
void meow_select_set_on_change(meow_select *s, meow_select_fn cb, void *ctx);
/* 打开模态(阻塞到确认/取消):选中 → 置 result=idx + 调 on_change(idx)+ 返回 idx;
* 取消/点面板外/左缘返回 → 返回 -1(不调 on_change)。 */
int meow_select_open(meow_select *s);
void meow_select_free(meow_select *s);
/* ---- 一次性(无回调,返回值即选择)----
* opts=选项数组(const char* 数组);n=个数;cur=初始选中。返回选中下标/-1=取消。 */
int meow_select_show(const char *title, const char *const *opts, int n, int cur);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_SELECT_H */
+75
View File
@@ -0,0 +1,75 @@
/*
* meow_ui_anim.h — SDK 动画基座 + UI 节拍器(§12.2:meow_anim / meow_anim_tick)。
*
* 定位:给应用提供**时间驱动 tween**(from/to/duration/easing)+ 完成/取消回调,并行多动画
* (静态池≤MEOW_ANIM_MAX)+ 与脏区集成(动画帧只标动画影响区,不做整屏重绘)。
*
* 节拍:**挂既有 on_tick 机制**(meow_ui_run 主循环 / meow_ui_feed_tick 阻塞 IME 循环),
* 无常驻高频轮询;无动画时 meow_anim_tick() 只做一个计数检查即返回(零开销)。
*
* 内存纪律:静态池(≤MEOW_ANIM_MAX),start/cancel/tick 全程零 malloc。
*
* 与 §7 的关系:§7(B1 最小可用)是「应用自己在 on_tick 里用 meow_ui_time_ms 做时间轴」;
* 本模块把这件事收敛成声明式 tween 对象 + 结束回调,是 §7 的**正式基座**(应用可任选其一或混用)。
*/
#ifndef MEOW_UI_ANIM_H
#define MEOW_UI_ANIM_H
#include "meow_ui_types.h" /* 基础类型(子头不引伞头,避免相对路径依赖) */
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 缓动曲线(t∈[0,1] → 曲线化的进度;OVERSHOOT 在中后段可略>1、末点恰=1)---- */
typedef enum {
MEOW_EASE_LINEAR = 0,
MEOW_EASE_OUT = 1, /* 快进慢停(decelerate) */
MEOW_EASE_IN_OUT = 2, /* 慢入慢出 */
MEOW_EASE_OVERSHOOT = 3, /* 越过目标再回稳(小回弹) */
} meow_ease;
float meow_ease_apply(meow_ease e, float t);
/* ---- 动画对象(不透明句柄;静态池)---- */
#define MEOW_ANIM_MAX 16
typedef struct meow_anim meow_anim;
/* 每帧回调:v = 当前插值值(from + (to-from)*ease(t))。app 在此更新状态并绘制。 */
typedef void (*meow_anim_update_fn)(meow_anim *a, float v, void *ctx);
/* 完成/取消回调:canceled=1=被取消,0=自然完成。 */
typedef void (*meow_anim_done_fn)(meow_anim *a, int canceled, void *ctx);
typedef struct meow_anim_desc {
float from;
float to;
uint32_t duration_ms; /* 时长(ms);0 视为瞬发(下一 tick 完成) */
meow_ease ease;
meow_anim_update_fn update; /* 可 NULL */
meow_anim_done_fn done; /* 可 NULL */
void *ctx;
} meow_anim_desc;
/* 启动一个动画(空闲槽;池满返回 NULL)。start 即置 value=from 并回调一次 update(0)。 */
meow_anim *meow_anim_start(const meow_anim_desc *d);
/* 取消(触发 done(canceled=1),不再 update)。 */
void meow_anim_cancel(meow_anim *a);
/* 查询:仍在运行(value 未达终点)。 */
int meow_anim_active(meow_anim *a);
/* 查询:当前插值值。 */
float meow_anim_value(meow_anim *a);
/* 与脏区集成:动画每 tick 只 flush 影响区(x,y,w,h;x<0 表示不自动 flush)。
* app 的 update 回调通常也会用 meow_ui_flush_region 自刷;设了这个则 tick 自动刷一次,
* 保证「动画帧只标脏动画影响区,不整屏」。 */
void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h);
/* ---- 节拍器(挂在 on_tick / meow_ui_feed_tick;无动画零开销)---- */
void meow_anim_tick(void); /* 推进所有活动动画(完成/取消在内部处理) */
unsigned meow_anim_count(void); /* 活动动画数(0=空闲) */
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_ANIM_H */
+9
View File
@@ -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);
+113
View File
@@ -0,0 +1,113 @@
/*
* meow_ui_box.h — SDK 线性布局引擎(§12.1:meow_box)。
*
* 定位:既有的 meow_row/column/box(§5.8.5,W_LAYOUT)只支持「固定尺寸子项 + 居中」,
* 本模块是其**独立增强**:竖直/水平容器,子项 = 固定尺寸 | weight 权重 | wrap(内容测量),
* 支持 margin/padding / 主轴对齐(start/center/end/space-between/around/evenly)/ 交叉轴对齐
* (start/center/end/stretch)/ 嵌套。
*
* 与绝对定位完全兼容:meow_box 是**纯布局计算器**,不是控件,不参与 z 序注册表/命中/绘制;
* arrange 把每个子控件的 x/y/w/h 写进现有 meow_widget(用新增 meow_widget_set_rect),
* 此后照常 draw/hit/flush_region/owner 闸门——与既有控件绝对定位零冲突。
*
* 与视图框架集成(opt-in):meow_box 可作 meow_view 的布局提供者(meow_view_set_layout);
* view 不设就保持现有绝对坐标行为(§12.1.4)。
*
* 内存纪律:静态池 + 定长 items 数组(≥MEOW_BOX_MAX_CHILD),热路径(arrange)零 malloc;
* 嵌套用深度≤MEOW_BOX_MAX_DEPTH 的静态栈单遍求解,无递归爆炸。
*/
#ifndef MEOW_UI_BOX_H
#define MEOW_UI_BOX_H
#include "meow_ui_types.h" /* meow_widget 前向 + 基础类型(子头不引伞头,避免相对路径依赖) */
#ifdef __cplusplus
extern "C" {
#endif
typedef struct meow_box meow_box;
/* 主轴方向 */
typedef enum {
MEOW_BOX_H = 0, /* 横向(row) */
MEOW_BOX_V = 1, /* 纵向(column) */
} meow_box_dir;
/* 子项主轴尺寸策略 */
typedef enum {
MEOW_BOX_FIXED = 0, /* 固定尺寸:内容主轴向取控件当前 w/h */
MEOW_BOX_WEIGHT = 1, /* 权重:均分剩余主轴空间(weight>0) */
MEOW_BOX_WRAP = 2, /* wrap:按内容测量(min/preferred 或 measure 回调) */
} meow_box_size;
/* 主轴对齐 */
typedef enum {
MEOW_BOX_ALIGN_START = 0,
MEOW_BOX_ALIGN_CENTER = 1,
MEOW_BOX_ALIGN_END = 2,
MEOW_BOX_ALIGN_SPACE_BETWEEN = 3,
MEOW_BOX_ALIGN_SPACE_AROUND = 4,
MEOW_BOX_ALIGN_SPACE_EVENLY = 5,
} meow_box_align;
/* 交叉轴对齐 */
typedef enum {
MEOW_BOX_CROSS_START = 0,
MEOW_BOX_CROSS_CENTER = 1,
MEOW_BOX_CROSS_END = 2,
MEOW_BOX_CROSS_STRETCH = 3,
} meow_box_cross;
/* 内容测量回调(size==MEOW_BOX_WRAP 时用;可 NULL):给出主轴向 min/preferred 尺寸。
* ctx = meow_box_add_ex 的 mctx。 */
typedef void (*meow_box_measure_fn)(void *ctx, int *min_main, int *pref_main);
/* 子项描述(add/add_ex/add_sub 提供;app 只读)。w 与 sub 二选一。 */
typedef struct meow_box_item {
meow_widget *w; /* 子控件(sub==NULL 时用) */
meow_box *sub; /* 嵌套 box(w==NULL 时用) */
int size; /* MEOW_BOX_* 尺寸策略 */
int weight; /* size==WEIGHT 时>0 */
int min; /* size==WRAP 的主轴下限 */
int preferred;/* size==WRAP 的主轴目标 */
meow_box_measure_fn measure; /* size==WRAP 内容测量(可 NULL) */
void *mctx;
} meow_box_item;
#define MEOW_BOX_MAX_CHILD 24 /* 每容器子项上限(静态数组) */
#define MEOW_BOX_MAX_DEPTH 16 /* 嵌套深度上限(静态求解栈) */
/* ---- 生命周期(静态池;new 冷路径,arrange 热路径零 malloc)----
* 注:命名用 meow_box_new(meow_box_create 已被 §5.8.5 的 W_LAYOUT box 占用)。 */
meow_box *meow_box_new(meow_box_dir dir);
void meow_box_free(meow_box *b);
/* ---- 容器配置 ---- */
void meow_box_set_rect(meow_box *b, int x, int y, int w, int h); /* 容器矩形(绝对坐标) */
void meow_box_set_padding(meow_box *b, int l, int t, int r, int btm);
void meow_box_set_gap(meow_box *b, int gap);
void meow_box_set_align(meow_box *b, int main_align, int cross_align);
/* ---- 子项添加(返回 0=成功;越界/无效=负值,不清空既有子项)---- */
int meow_box_add(meow_box *b, meow_widget *w, int size, int weight);
/* add_ex:WRAP 用 min/preferred(主轴向)+ 可选内容测量回调。 */
int meow_box_add_ex(meow_box *b, meow_widget *w, int size, int weight,
int min, int preferred, meow_box_measure_fn m, void *mctx);
/* 嵌套子 box(parent 布局会为 sub 分配矩形并递归 arrange)。 */
int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight);
/* ---- 求解(单遍;深度≤MEOW_BOX_MAX_DEPTH 静态栈,无递归)----
* 把每个子控件 x/y/w/h 写进其 meow_widget(meow_widget_set_rect);
* 嵌套 sub 的矩形写进 sub 并递归。子控件的 margin(meow_widget_opts.margin_x/y)参与排布。 */
void meow_box_arrange(meow_box *b);
/* ---- 查询 ---- */
int meow_box_count(const meow_box *b);
meow_widget *meow_box_widget_at(const meow_box *b, int idx); /* 子项控件(无则 NULL) */
meow_box *meow_box_sub_at(const meow_box *b, int idx); /* 子项嵌套 box(无则 NULL) */
void meow_box_rect(const meow_box *b, int out[4]); /* x,y,w,h */
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_BOX_H */
+18
View File
@@ -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 最小可用) */
/* ============================================================ */
+64 -3
View File
@@ -1,7 +1,8 @@
/*
* meow_ui_image.h — SDK 图片层(§4,B1)。
*
* 原 meow_ui.h 拆分的一部分(image 模块)。内部 stb_image(单头文件,静态编入 meow_ui.a)
* 原 meow_ui.h 拆分的一部分(image 模块)。解码后端:M23 真缩解(JPEG DCT 域 / PNG 逐行 /
* BMP 跳行,见 meow_jpeg.h)优先,回退 stb_image(单头文件,静态编入 meow_ui.a)。
* 解 PNG/JPEG/BMP,统一转 RGBA。meow_img 为不透明句柄(前向声明见 meow_ui_types.h)。
*/
#ifndef MEOW_UI_IMAGE_H
@@ -22,10 +23,70 @@ meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解
meow_img *meow_img_load_mem(const void *buf, int len);
void meow_img_free(meow_img *img);
int meow_img_w(meow_img *img);
int meow_img_h(meow_img *img);
/* 追加(M12)预算解码:只读文件头取原始尺寸,不解码像素(零像素内存)。成功 1,失败 0。
* App 可用它在加载前判断「图太大」并给用户提示,而不必先撞 OOM。 */
int meow_img_probe_file(const char *path, int *w, int *h, int *channels);
/* 追加(M12/M23)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim,尺寸与
* 「meow_img_load_file + meow_img_downscale(maxdim)」逐字节同口径(公式一致)。
*
* 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率:
* JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值;
* N=1..7,N≥8 交回 stb),渐进式跳过精细化扫描、只解各频带首次扫描 —— 低频带已完整,
* 不整幅驻留系数阵列;
* PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/
* 调色板/1-16bit,16bit 取高字节,tRNS 透明含 8bit 灰/RGB);
* BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。
* 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。
* 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、过订阅 Huffman 表、16bit 量化表、Adam7
* 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸)自动回退 stb 旧路径,对外语义完全不变。
* 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。
*
* 尺寸口径说明(M23 评审 P2-2):本入口按「原图 + meow_img_downscale」公式算目标尺寸;M12 旧
* 实现经 stb 的 ceil(w/s) 缩解再精修,源尺寸不是档位整数倍时可能与本入口差 1px(实测 1999×2999:
* 旧 320×480 vs 新 319×480)。阅读器按显示框缩放,无实际影响;PNG/BMP 无此差异。 */
meow_img *meow_img_load_file_dim(const char *path, int maxdim);
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim);
/* 追加(M22)缩解档位选择:取满足「长边/档位 ≥ maxdim」的最大整数档 s ∈ {1,2,4,8},
* 保证缩解结果不欠采样。meow_img_load_file_dim 内部用的就是这个函数;调用方(阅读器)
* 用它枚举候选档位做内存预算,避免两处各写一份而漂移。 */
int meow_img_pick_scale(int longside, int maxdim);
/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim,
* 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、
* 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。
* M23 起:先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶),
* 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。
* M27 起 PNG/BMP 也走同一后端入口(阅读器峰值模型已同步为「目标图 + 工作面」口径);
* 断言与实测见 tuku/test/test_reader_membudget.c 的 B7。 */
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step);
/* 追加(M27):与 meow_img_load_file_step 相同,额外控制回退并回报**实际**路径:
* allow_stb_fallback = 1:后端不支持/失败时回退 stb(与 _step 完全一致);
* allow_stb_fallback = 0:只尝试后端,失败返回 NULL——**绝不**回落 stb。阅读器的门控
* 按「后端峰值」放行时必须用 0,否则头部判定通过、解码中途失败的文件会静默走 stb,
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG)。失败页由调用方
* 按坏图占位。
* used_backend = 1(M23 后端直出目标尺寸)/ 0(未走成后端)。NULL 时等价 _step(允许回退)。 */
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
int allow_stb_fallback, int *used_backend);
/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的
* 结果一次性落到显示框,使命中页仍是单次原生 blit(不做每帧重采样)。同尺寸返回 NULL
* (调用方保留原图);失败返回 NULL。 */
meow_img *meow_img_resample(meow_img *im, int w, int h);
int meow_img_w(const meow_img *img);
int meow_img_h(const meow_img *img);
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque,
* 本函数为其唯一像素读入口;NULL 返回 NULL。 */
const unsigned char *meow_img_pixels(const meow_img *img);
void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */
/* 内存瘦身:最近邻等比缩放到最长边 ≤maxdim,返回新图;原图已 ≤maxdim 返回 NULL。
* 调用方负责 free 旧图与返回值(若返回)。封面/背景大图驻留内存瘦身用。 */
meow_img *meow_img_downscale(meow_img *img, int maxdim);
void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img); /* 最近邻缩放 */
void meow_ui_img_alpha(int x, int y, int w, int h, meow_img *img, int alpha);
void meow_ui_img_cover(int x, int y, int size, meow_img *img); /* 居中裁方 → size×size */
+61
View File
@@ -36,6 +36,13 @@ void meow_ui_request_exit(void); /* 请求退出(相当于返回/
* x<=0 时清掉此前画的条(填回背景色)。不发 flush,由调用方 meow_ui_flush_region 上屏。 */
void meow_ui_swipe_hint(int x);
/* 批②(2026-09-04):底部热区上滑 → 回桌面(home)判定,镜像 launcher np_gesture.h
* np_home_gesture 同口径。down_x/down_y=抬手原点,dx/dy=抬手位移(释放-原点),
* fb_h=屏高(SDK 传 meow_ui_height())。命中=起点在底部热区(最后 HOME_EDGE_H≈28px)且
* **不在左缘**(down_x>=EDGE_W,与左缘返回互斥)+ 上移 ≥HOME_SWIPE(65px) + 竖向主导(adx<-dy)。
* 供宿主单测直调;触摸状态机(meow_ui_input.c touch_release)与 App 内判据共用,避免两端漂移。 */
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h);
/* ============================================================ */
/* §6.3 通用长按(2026-08-31) */
/* ============================================================ */
@@ -58,6 +65,60 @@ void meow_ui_set_long_press_cb(meow_long_press_fn cb); /* 注册/更换回调
void meow_ui_set_long_press_ms(int ms); /* 长按阈值(ms);<=0 恢复默认 500 */
int meow_ui_long_press_ms(void); /* 当前阈值(默认 500) */
/* ============================================================ */
/* §6.4 事件分发形式化(2026-09-02)—— 统一「命中测试 / 捕获 / 消费 / 层间隔离 / 焦点」
*
* 背景:此前命中/捕获/消费/层间隔离各处 ad-hoc 补丁(模式层穿透、弹层叠黑、list 焦点残留、
* 同坐标二次消费)。本节把三层模型的形式化落地为**单一仲裁者**(SDK 内部实现,见
* devdocs/plan/SDK_VIEW_FRAMEWORK_DESIGN.md §A.7 命中分流 ③②① + 本 SDK 内部拓扑):
*
* 触摸沿 z 序**自顶向下**命中:模态层永远先审;某一层命中即消费(返回 consumed 才继续下探)。
* 某一次 touch-down 命中后,捕获直到 up;期间 move/up 只发给捕获目标(防滑出误触发下层)。
* 模态层存在时下层**完全收不到**事件——未命中面板的事件由模态背板消费,不透传。
*
* 兼容:**只追加、不删改既有签名**;未调用本节 API 的 App 行为与之前完全一致(命中/捕获仍
* 由 App 在 on_tap/on_touch_* 里自理)。hello/files/ncm/reader 等既有 App 只把 SDK 提供的
* 语义用于收敛 bug,不改正常交互。
*/
/* ---- 模态作用域(modal scope)----
* 一组自顶向下优先的叠层。dialog/sheet/App 自绘模态在「打开时 begin、关闭时 end」。
* 作用域存在(meow_ui_modal_active / depth>0)时,下层(view/widget)完全收不到触摸;
* 未命中面板的事件由背板消费(meow_ui_hit_topdown 恒返回模态哨兵)。支持嵌套(弹层叠开,
* depth 递增)。begin 返回作用域句柄;end 销栈(幂等,NULL 忽略)。 */
typedef struct meow_ui_modal meow_ui_modal;
meow_ui_modal *meow_ui_modal_begin(void); /* 打开模态层入栈;返回句柄(NULL=栈满/上限) */
void meow_ui_modal_end(meow_ui_modal *m); /* 关闭:弹到 m(含 m,一次性关上嵌套) */
int meow_ui_modal_active(void); /* 当前是否有任一模态作用域打开(depth>0) */
int meow_ui_modal_depth(void); /* 模态栈深(弹层叠开计数) */
/* ---- 自上而下命中 ---- */
/* 沿 z 序(模态最高优先,其次注册控件 z 降序;均含 owner==active+enable 过滤)返回第一个
* 命中绘制物(meow_widget* 或 meow_view*);无命中返回 NULL。模态存在时恒返回模态哨兵
* (背板消费,不透传)。供 gesture_begin/focus_tap/App 自绘模态命中统一使用。 */
void *meow_ui_hit_topdown(int x, int y);
/* meow_widget_hit 的公共别名(owner==active + enable + 矩形/自定义命中回调一致)。 */
int meow_ui_widget_hit(meow_widget *w, int x, int y);
/* ---- 手势捕获(gesture capture)----
* touch-down 命中目标捕获整个手势直到 up。捕获期 meow_ui_gesture_owner 恒返回捕获目标;
* 后续 move/up 只发给它(防滑出后误触发下层)。begin 返回捕获目标(未命中=NULL 不捕获);
* end/抬手释放。 */
void *meow_ui_gesture_begin(int x, int y); /* 命中+捕获;返回捕获目标(模态存在时=模态哨兵) */
void *meow_ui_gesture_owner(void); /* 当前手势捕获目标(无=NULL) */
void meow_ui_gesture_end(void); /* 手势结束(抬手/TAP/SWIPE 边沿):释放捕获 */
/* ---- 唯一焦点(focus,root 根治 list 焦点残留 / 滚动容器间转移)----
* 焦点持有者唯一。点非焦点区域 → 释放焦点并触发 lost 回调;滚动容器(list/scroll)在
* 获得焦点时 acquire,点外由 meow_ui_focus_tap 统一释放——「命中非 focus 区域 → 释放并通知」。
* 转移规则:某滚动容器 acquire 后,随后的纵向 swipe / 拖动滚该容器;任何落在该容器外的
* 触摸(hit != focus owner)释放焦点,回退到页面级滚动。 */
typedef void (*meow_ui_focus_lost_fn)(void *owner, void *ctx); /* 焦点被释放/转移时回调 */
void meow_ui_focus_set_lost_cb(meow_ui_focus_lost_fn cb, void *ctx); /* 注册/更换焦点丢失回调 */
void *meow_ui_focus_owner(void); /* 当前唯一焦点持有者(无=NULL) */
int meow_ui_focus_acquire(void *owner); /* 设为唯一焦点(覆盖旧焦点并触发其 lost) */
int meow_ui_focus_release(void *owner); /* owner==当前焦点才释放+通知(NULL=任意) */
void meow_ui_focus_tap(int x, int y); /* TAP 落点:命中焦点持有者→保位;否则释放+通知 */
#ifdef __cplusplus
}
#endif
+33
View File
@@ -0,0 +1,33 @@
/*
* meow_ui_net.h — MeowOS SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;追加式 API,含入 meow_ui.h)。
*
* 定位:框架 App 联网前的**自愿自治**通道 + **自愿字节上报**,二者都不强制(防君子不防小人)。
* - meow_net_policy_check(host):联网前自查 host 是否被 netmgrd 管控策略放行。
* 返回 0=allow(可联网);1=block(策略拒绝,App 应中止本次联网)。
* netmgrd 不在 / 超时 → 0(乐观放行,绝不因监控不可用而误封业务)。
* - meow_net_report(rx,tx):App 自愿上报本进程累计收发字节。内核无 per-socket/per-uid
* 字节计数器(NETMGR_DESIGN §4 表),§4 的「字节级精确记账走 SDK 自报通道」由此落地;
* netmgrd 记入 per-实体统计(status JSON `entity_bytes`)。best-effort,失败静默不影响业务。
* 用法:App 在发起 HTTP/TLS 前调 meow_net_policy_check(host),若返回 1 则按产品语义处理(如提示被限)。
*/
#ifndef MEOW_UI_NET_H
#define MEOW_UI_NET_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 检查 host 是否被策略放行。0=allow;1=block;netmgrd 不在/超时 → 0(乐观放行)。
* host 为域名(如 "music.163.com");判定以当前 App(MEOW_LOG_APP)为实体,服务端策略引擎权威执行。 */
int meow_net_policy_check(const char *host);
/* 上报本进程累计收发字节(best-effort,失败静默)。 */
void meow_net_report(uint32_t rx, uint32_t tx);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_NET_H */
+65
View File
@@ -0,0 +1,65 @@
/*
* meow_ui_scroll.h — SDK 滚动物理(§12.3:meow_scroll)。
*
* 定位:给既有滚动容器(W_LIST / W_LISTVIEW / LazyColumn / LazyRow)加**可惯性滚动**的
* **物理层**——速度跟踪(最近 N 个采样加权)、松手 fling(减速度曲线,用 §12.2 动画基座驱动)、
* 边界过冲 clamp + 简单回弹(不做复杂 spring)、滚动停止可选 snap。**跟手拖动行为保持现状
* 不变**(track 内部调既有 set_scroll,clamp 一致)。
*
* 设计:meow_scroll 是**滚动会话**,包住一个滚动控件(或任意的 set/max/get/redraw 回调)。
* 拖动态:app 在 on_touch_move 里把位移喂给 meow_scroll_track(dy, t)(等价于 app 直接调
* meow_list_scroll_by,且额外记录速度采样);松手:app 调 meow_scroll_fling(s),内部用
* 加权速度采样得出初速 → 起一条 ease-out 动画推进位移 → 每帧只 redraw(flush 列表视口区)。
*
* 内存纪律:静态池(应用侧按需创建会话),track/fling/tick 全程零 malloc;无动画时
* meow_scroll 不参与节拍(fling 由 meow_anim_tick 驱动,活动计数为 0 则零开销)。
*/
#ifndef MEOW_UI_SCROLL_H
#define MEOW_UI_SCROLL_H
#include "meow_ui_types.h" /* meow_widget 前向 + 基础类型(子头不引伞头,避免相对路径依赖) */
#ifdef __cplusplus
extern "C" {
#endif
typedef struct meow_scroll meow_scroll;
/* ---- 通用回调(wrap 之外的自定义滚动源用)---- */
typedef void (*meow_scroll_set_fn)(void *ctx, int off);
typedef int (*meow_scroll_max_fn)(void *ctx);
typedef int (*meow_scroll_get_fn)(void *ctx);
typedef void (*meow_scroll_redraw_fn)(void *ctx); /* 只刷滚动视口区(不整屏) */
/* 创建会话:app 提供 set/max/get/redraw。返回 NULL=池满。 */
meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
meow_scroll_get_fn get, meow_scroll_redraw_fn redraw,
void *ctx);
/* 便捷:包一个 meow_widget 列表(is_listview:1=W_LISTVIEW / 0=W_LIST 或 lazycolumn/row)。
* redraw 只 flush 该控件矩形(视口区)。 */
meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview);
void meow_scroll_free(meow_scroll *s);
/* 拖动跟手 + 速度采样:dy=本次位移(px);t_ms=采样时刻(单调毫秒)。
* 内部 set_scroll(pos+dy)(clamp 到 [0,max],与 app 直接 scroll_by 行为一致)。 */
void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms);
/* 松手 fling:内部用「最近 N 个采样加权」算初速,起减速度曲线动画推进位移;
* 边界过冲 clamp + 简单回弹;每帧只 redraw 视口区。停止后可选 snap。 */
void meow_scroll_fling(meow_scroll *s);
/* 立即停止(取消 fling/回弹/snap,停在当前 offset)。 */
void meow_scroll_stop(meow_scroll *s);
/* 当前加权初速(px/s;为 fling 内部所用,供 app 读取/调试)。 */
int meow_scroll_velocity(meow_scroll *s);
/* 查询/配置 */
int meow_scroll_active(meow_scroll *s); /* 是否有活动 fling/回弹/snap */
int meow_scroll_offset(meow_scroll *s); /* 当前 scroll(get) */
void meow_scroll_set_snap(meow_scroll *s, int step); /* >0=fling 停止后吸附到 step 倍数 */
void meow_scroll_set_friction(meow_scroll *s, int tau_ms); /* 摩擦时间常数(默认 180ms) */
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_SCROLL_H */
+55
View File
@@ -0,0 +1,55 @@
/*
* meow_ui_str.h — SDK 字符串资源(i18n 雏形,§13.meow_str_*)。
*
* 定位:公开 SDK 前,系统/App UI 文案不得再硬编码在 C 源码里——第三方开发者必须能按
* 系统 locale 拿到对应语言文案,且能在不改 SDK 二进制的情况下用 TF 卡覆盖/追加。
* 本头声明字符串资源框架的**对外 API**(追加式,既有 App 零改动)。
*
* 单表双语内置(zh_cn = 内置默认 / en_us),四层取值优先级(见 meow_ui_str.c):
* meow_str(id):按当前 locale 取串;override 覆盖表 > 内嵌当前语言 > 内嵌内置默认(zh_cn);
* 全程不可得则原样返回 id(调用方永不见 NULL)。
* meow_strf(out,outsz,id,...):取串后按 snprintf 语义格式化进调用方缓冲(自带 NUL 终止)。
* meow_str_set_locale(locale):切当前 locale("zh_cn"|"en_us"),即时生效 + 重载 TF 覆盖表
* + best-effort 持久化到板载 settings.conf 的 ui.locale。
*
* 依赖:纯 libc(stdio/stdlib/string/stdarg),宿主 gcc 可直接编译单测;不依赖其它 SDK 对象。
* - locale 初值:启动首次调用惰性读系统设置 settings.conf 的 `ui.locale`,缺省 "zh_cn"。
* - TF 覆盖表:`<MEOW_DATA_DIR>/../strings/<locale>.txt`(即 MeowData/<pkg>/strings/,见
* §9.2 数据目录约定),`id=text` 行格式 UTF-8;无卡/无文件/不含该 id → 内嵌表兜底。
* - 热路径(meow_str)零 malloc:内嵌为编译期静态表,覆盖表为启动加载的静态数组。
*
* 边界(§13 明示不做,写清理由):
* 不做复数规则 / BiDi / RTL / 动态字体回退——当前形态是「按 locale 取整串」的 i18n 雏形,
* 面向 480×720 单行/短文案 UI;复数变体需占位符格式化协议,BiDi/RTL 需文本整形 + 布局镜像,
* 两者都超出「字符串资源外置」的最小必要范围,留待后续批次。
*/
#ifndef MEOW_UI_STR_H
#define MEOW_UI_STR_H
#ifdef __cplusplus
extern "C" {
#endif
/* 按当前 locale 取 id 对应的文案。id 形如 `域.名`(common.ok / player.pause)。
* 返回静态字符串(内嵌表或覆盖表;永不返回 NULL)。未知 id 原样返回 id。 */
const char *meow_str(const char *id);
/* 取 id 文案后按 snprintf 语义格式化进 out(outsz 为缓冲字节数,含 NUL)。
* 返回 snprintf 样式的「应写入字符数」(不截断);out==NULL 或 outsz<=0 返回 -1。
* 设计理由:调用方自带缓冲,避免静态/线程局部缓冲在 IME/模态等重入路径的竞态,
* 且与 SDK 既有 snprintf 习惯一致(无格式参数时等价深拷贝 meow_str 结果)。 */
int meow_strf(char *out, int outsz, const char *id, ...);
/* 设当前 locale(识别 "zh_cn"|"en_us";任意串作为覆盖表文件名 + 内嵌按非 en 处理为内置默认)。
* 即时生效(下次 meow_str 即按新 locale);重载该 locale 的 TF 覆盖表;
* best-effort 持久化到 settings.conf 的 ui.locale(失败静默,运行时仍切换)。
* 须在 App 主线程(UI 线程)调用,避免与 meow_str 并发写覆盖表。 */
void meow_str_set_locale(const char *locale);
/* 当前生效 locale(内部缓冲;未初始化时读 settings 后返回,默认 "zh_cn")。 */
const char *meow_str_locale(void);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_UI_STR_H */
+22
View File
@@ -62,6 +62,28 @@ void meow_theme_remove_listener(meow_theme_listener cb);
/* [D4] 脱机访问两套内置色板(无 settings 文件也可靠):返回当前解析生效的色板副本。 */
meow_theme meow_theme_defaults(meow_theme_mode m);
/* ---- §8.1.5 色板导出/注入(单一权威桥,M7.0a)----
* 用户在 M7 定案「SDK meow_theme 为唯一权威」:launcher 的 s_ui_theme() 将改为读 SDK 的薄壳,
* 不再自持 4 套静态色板。本接口是那个前置(launcher 侧改动不在此批)——
*
* meow_theme_export(out) 把当前「style×mode」解析生效的完整权威主题拷给调用方。
* 调用方是 stateless 的绘制原语时(如 launcher s_ui_theme),由此读
* 单一权威,再按它的角色字段(9 色)映射到自己的绘制便利结构,即:
* surface → surface
* surfaceVariant → surface_variant
* surfaceContainer → surface_container
* primary → primary
* onSurface → on_surface
* onSurfaceVariant → on_surface_variant
* outline → outline
* error → error
* surfaceTint → surface_tint
* meow_theme_inject(src) 用一份自定义板覆写当前生效主题(如 launcher 自定义背景/强调/风格),
* src==NULL 恢复内建(随 settings 解析)。覆写不随 meow_theme_reload() 丢弃
* (程序化覆写优先于 settings,与 meow_ui_set_style 语义一致)。 */
void meow_theme_export(meow_theme *out);
void meow_theme_inject(const meow_theme *src);
/* ---- §8.1 顶级主题风格族(minimal,2026-08-30)----
* 主题 = 顶级主题风格族(ui.style:MD3/MIUIx)× 明暗(display.theme)。本批只落「状态栏图标集
* 随 ui.style 换集」的最小闭环:meow_ui_set_style 记录 style(同值忽略)+ 触发 theme_reload/
+135
View File
@@ -33,6 +33,8 @@ void meow_widget_free(meow_widget *w);
void meow_widget_draw(meow_widget *w); /* 每次 flush 前调用 */
int meow_widget_hit(meow_widget *w, int x, int y); /* 点是否落在控件内 */
/* 写控件矩形(§12.1 meow_box 布局引擎用;也可供 app 布局器用)。只设字段不标脏。 */
void meow_widget_set_rect(meow_widget *w, int x, int y, int wd, int ht);
void meow_slider_set(meow_widget *s, int val);
int meow_slider_get(meow_widget *s);
@@ -47,6 +49,7 @@ int meow_btn_pressed(meow_widget *w); /* 应用在 on_tap 命中后
/* ---- §5.8.2 Modifier 式通用 Options(轻量结构体,非链式 DSL)----
* 经 meow_widget_set_opts 一次性挂载,作用于任意控件(标准/自定义)。全部控件共用。
* 哨兵语义:bg/fg==0=用控件/主题默认;stroke==0=无描边;stroke_w=描边宽;
* 控件语义:W_PROGRESS 用 bg=轨道(0=surfaceVariant)、fg=填充(0=primary);见 meow_ui_widget.c 批5b;
* radius==-1=控件默认(如按钮 10),>=0=指定圆角(0=直角);pad/margin=0=无;
* disabled/selected=1=置位(立即反映到状态机位)。 */
typedef struct meow_widget_opts {
@@ -143,6 +146,17 @@ enum {
meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *label, int variant);
int meow_btn_set_variant(meow_widget *w, int variant); /* 越界 clamp;0=成功 */
/* ---- MD3 shape 层级(§5.8.6,Material You 圆角体系,单位 px)----
* 组件按角色落到五档 + 胶囊:按钮=full(全高一半)、对话框=extra-large(28)、卡=large(16)、
* 输入框=small(8)、勾选=extrasmall(4)。theme.radius/radius_card 为风格族默认;App 可经
* opts.radius 逐控件覆盖。全高一半用 MEOW_SHAPE_FULL(h) 宏(控件的胶囊圆角)。 */
#define MEOW_SHAPE_EXTRASMALL 4 /* 勾选 Checkbox/Radio */
#define MEOW_SHAPE_SMALL 8 /* 输入框 TextField */
#define MEOW_SHAPE_MEDIUM 12 /* 主题默认 radius(列表卡/通用控件) */
#define MEOW_SHAPE_LARGE 16 /* 卡片/容器(theme.radius_card 默认) */
#define MEOW_SHAPE_EXTRA_LARGE 28 /* 对话框 Dialog(MD3 标准 extra-large) */
#define MEOW_SHAPE_FULL(h) ((h) / 2) /* 全形=胶囊(按钮/导航等) */
/* ---- TextField(单行文本输入,接 IME §5.1)---- */
/* placeholder=提示文案;buf=App 持有的文本缓冲(组件不回拷,只绘制/编辑其上);maxlen=含 NUL 预算 */
meow_widget *meow_textfield_create(int x, int y, int w, int h, const char *placeholder,
@@ -184,6 +198,40 @@ void meow_topappbar_set_back(meow_widget *w, int on); /* 1=显示返回箭头
int meow_topappbar_add_action(meow_widget *w, const char *label); /* 返回动作下标 */
#define MEOW_TOPBAR_BACK (-2)
int meow_topappbar_hit(meow_widget *w, int x, int y); /* MEOW_TOPBAR_BACK=返回 idx>=0=动作 -1=无 */
/* TopAppBar 位图动作图标(sb_icons[],Material Symbols Rounded 32x32 单通道 alpha,Apache-2.0)。
* 动作项默认文本(add_action);经本 API 把某项改成位图:绘制取主题 onSurface(按压淡色),
* 与状态栏同源 blit(ink 垂直对齐栏中点)。返回 0=成功;NULL/非 topbar/越界 idx/非法 icon=负值。 */
int meow_topappbar_set_item_icon(meow_widget *w, int idx, int icon_idx);
/* 位图动作图标索引(= sb_icons[] 下标,与 SDK sb_icons.h 的 SB_I_* 同值)。
* 状态栏占用 0-14(不动);动作图标从 15 起连续追加,勿改序(须与 sb_icons.h 表序一致)。 */
enum {
MEOW_ICON_SETTINGS = 15,
MEOW_ICON_REFRESH = 16,
MEOW_ICON_SEARCH = 17,
MEOW_ICON_PLAY_ARROW = 18,
MEOW_ICON_PAUSE = 19,
MEOW_ICON_SKIP_NEXT = 20,
MEOW_ICON_SKIP_PREV = 21,
MEOW_ICON_ARROW_BACK = 22,
MEOW_ICON_CLOSE = 23,
MEOW_ICON_ADD = 24,
MEOW_ICON_CHECK = 25,
MEOW_ICON_LYRICS = 26,
MEOW_ICON_QUEUE_MUSIC = 27,
MEOW_ICON_DOWNLOAD = 28,
MEOW_ICON_HEART = 29,
};
/* ---- IconButton(圆形/位图图标按钮,M7.0a 缺口#2)----
* 圆形命中 + 位图图标(sb_icons[],Material Symbols Rounded 单通道 alpha)+ 按压态(实时)。
* 用途:电源菜单圆钮 / Now Playing 播放控制 / 锁屏常用件等「位图图标 + 圆形」按钮。
* icon_idx = sb_icons[] 下标(用上面 MEOW_ICON_* 枚举或 SB_I_*;越界画空位图,命中照常)。
* size = 按钮直径(外接正方形 w=h=size;图标 ink 高 = size*6/10,MD3 图标比)。
* 语义色:底 surfaceContainerLow、图标 onSurface;按压叠 onSurface 状态层(实时,同 meow_btn);
* 禁用变淡;选中态 primaryContainer/onPrimaryContainer(meow_widget_set_selected)。
* 命中:圆形(半径=size/2,圆心=按钮中心),owner==active + enabled 双闸门。
* 返回按钮句柄;App 在 on_tap 里用 meow_widget_hit 判定是否命中本钮(命中即触发动作)。 */
meow_widget *meow_iconbtn_create(int x, int y, int size, int icon_idx);
/* ---- NavigationBar(底部导航栏)---- */
meow_widget *meow_navigationbar_create(int x, int y, int w, int h);
@@ -218,6 +266,10 @@ int meow_list_selected(meow_widget *w);
typedef void (*meow_list_tap_fn)(int idx, void *ctx);
void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx); /* 命中行→回调 */
int meow_list_tap_at(meow_widget *w, int x, int y); /* 命中行:置选中+回调,返回 idx/-1 */
/* MD3 容器模式(W_LIST 共用,§5.5/§5.8.6):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
int meow_list_set_container(meow_widget *w, int on);
/* ============================================================ */
/* §5.5 设计模板 A② 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作 + 选中
@@ -291,6 +343,89 @@ void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ct
/* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */
int meow_listview_tap_at(meow_widget *lv, int x, int y);
/* ============================================================ */
/* §5.5 虚拟化列表(W_LISTVIEW 扩展层,2026-09-02):固定 item 池 + 多行富 item + 完全自绘
* ------------------------------------------------------------
* 在既有 W_LISTVIEW(src2 每帧按需取行,行为不变)之上做**虚拟化层**(API 追加式,旧 API 保留):
* - 数据源:count_fn() 返回总行数(任意长度,≥65535 不常驻 item 对象);
* bind_fn(index, item, ctx) 仅在行「进入视口」(绑定到固定池槽)时调用一次;
* - 固定 item 池:池容量 = 视口行数 + 2 静态槽(有界,见实现 MEOW_LV_POOL_MAX);
* 行离开视口不销毁,回滚不重绑(同一 index 命中同一槽 → bind 不重调);
* - 滚动只改 viewport 偏移(复用既有 set_scroll/max_scroll),不重绑未进视口的项;
* - 多行富 item:主标题+副标题(第二行小字)+左图标/缩略图+右尾件(chevron/开关/复选/⋯);
* 行高可配(set_item_h 精确 px / set_item_tier 单/双行档位默认高);
* - 默认 renderer 用主题语义色绘制富 item;App 也可经 set_item_draw 完全自绘。
* 段头:bind 回调把 item->section=1(title=段名)→ 段头占位行(非粘性),默认 renderer 特殊绘制。
* 空态:set_empty_text 设置文案,count==0 时视口内居中画出。
* 兼容:既有 meow_listview_*(src2 模式)全部保留不动;本层为叠加,src2==NULL 时走既有路径。 */
/* 虚拟化行 item(bind 回调填充;SDK 固定池持有;不常驻 src2 那样的全量行对象)。 */
typedef struct meow_lv_item {
const char *title; /* 主标题(必填,NULL 视为空行) */
const char *subtitle; /* 副标题(第二行小字),可空 */
const meow_img *icon; /* 可选:左图标/缩略图(icon_kind==MEOW_LIST_ICON_IMG 时用) */
int icon_kind; /* MEOW_LIST_ICON_*(NONE=无图标) */
int right; /* MEOW_LIST_RIGHT_* 右尾件 */
int on; /* right==SWITCH 时开关态(0/1) */
int enabled; /* 1=可交互;0=变淡不响应 */
int section; /* 1=段头占位行(title=段名;默认 renderer 特殊绘制,非粘性) */
void *user; /* App 私有(跨 bind 保存业务态/图/index;SDK 不碰) */
} meow_lv_item;
/* 虚拟化数据源:count 返回当前行数(>=0,任意长);bind 在行进入视口时被调一次填充 item。
* 二者均不常驻 item 对象;count 每帧实时查(支持搜索过滤/懒加载动态长度)。 */
typedef int (*meow_listview_count_fn)(void *ctx);
typedef void (*meow_listview_bind_fn)(int index, meow_lv_item *item, void *ctx);
/* 完全自绘回调(覆盖默认 renderer):idx=行号;x,y,w,h=行视口矩形(已裁剪到列表视口);
* state=MEOW_LISTVIEW_ST_* 状态位(选中/按压/禁用组合)。App 用 §3 绘制 API 自绘整行。 */
typedef void (*meow_listview_draw_fn)(int idx, int x, int y, int w, int h,
int state, void *ctx);
/* state 位(set_item_draw 的 draw 回调收到;选中/按压/禁用组合,供自绘整行)。 */
#define MEOW_LISTVIEW_ST_SELECTED (1u<<0)
#define MEOW_LISTVIEW_ST_PRESSED (1u<<1)
#define MEOW_LISTVIEW_ST_DISABLED (1u<<2)
/* 挂虚拟化数据源:切到 pool 模式(count_fn/bind_fn 均非 NULL 才生效;否则回退 src2 模式)。
* 返回 0=成功;NULL 回调/无效=负值。挂虚拟源后既有 sel/滚动/命中/焦点照常。 */
int meow_listview_set_vsource(meow_widget *lv, meow_listview_count_fn count,
meow_listview_bind_fn bind, void *ctx);
/* 默认 renderer 配置:lines=1/2(单行/双行档位,影响副标题显示与行高默认);
* show_icon=1 画左图标/缩略图;show_right=1 画右尾件。返回 0=成功。 */
int meow_listview_set_item_renderer(meow_widget *lv, int lines, int show_icon,
int show_right);
/* 行档位默认行高:lines=1→48(单行)、2→56(双行);与 set_item_h(精确 px)互补。 */
int meow_listview_set_item_tier(meow_widget *lv, int lines);
/* 完全自绘:设 draw_item 回调(覆盖默认 renderer);NULL=用默认 renderer。返回 0=成功。 */
int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw,
void *ctx);
/* MD3 容器模式(默认 renderer 可选,P0 对齐):0=扁平满宽行+分隔线(默认,兼容);
* 1=每行外套 surfaceContainer 圆角卡(12dp)+行间空隙,选中=primaryContainer/按压=primary α
* (tonal,非满幅实心),去冗余分隔线;段头=surface 上的 primary 段标签(无卡无分隔线)。
* 注意:仅对默认 renderer(未 set_item_draw)生效;完全自绘行由 App self-draw 自理。 */
int meow_listview_set_container(meow_widget *lv, int on);
/* 空态占位:count==0 时视口内居中画 empty_text(NULL 不画)。返回 0=成功。 */
int meow_listview_set_empty_text(meow_widget *lv, const char *text);
/* ============================================================ */
/* §5.5 左滑删除(M7.3 通知中心缺口 #9):列表行横向手势 + 删除/回弹动画 */
/* ------------------------------------------------------------
* 行自绘 set_item_draw 配合(SDK 在自绘行前把该行横向偏移 swipe_dx 传给 x,并画右露删除区)。
* 手势由 App 在 on_touch_move 喂三级:begin(touch-down)/ move(touch-move)/ end(release)。
* 判定:横向小位移|dx|>|dy| 且超容差=swipe(move 返回 1=本列表消费,App 勿滚动);纵向
* |dy|>|dx| 且超容差=滚动手势优先(返回 0 并取消本行 swipe,App 照常 scroll_by);点按
* 无位移(end 时未 started)=行点击(返回 0,App 照常 tap_at)。不修改 tap_at/滚动语义。
* 动画:超阈值 → 删除动画(行滑出 meow_anim,结束后回调 on_delete(idx));未超 → 回弹。
* 颜色:删除区用主题 error/onError;不与行点击/滚动冲突(见判定表)。 */
typedef void (*meow_listview_swipe_fn)(int idx, void *ctx);
void meow_listview_set_swipe_delete(meow_widget *lv, int enabled,
meow_listview_swipe_fn on_delete, void *ctx);
int meow_listview_swipe_begin(meow_widget *lv, int x, int y); /* touch-down:1=命中可滑行(喂后续 move) */
int meow_listview_swipe_move(meow_widget *lv, int x, int y); /* touch-move:1=消费(横向);0=让位(取消/纵向) */
int meow_listview_swipe_end(meow_widget *lv, int x, int y); /* release:1=消费(删/回弹动画);0=未滑动(tap) */
int meow_listview_swipe_active(meow_widget *lv); /* 1=有行处于滑动/动画中 */
int meow_listview_swipe_offset(meow_widget *lv); /* 当前滑动行的 dx(<0=左移;自绘行画用) */
#ifdef __cplusplus
}
#endif
+302
View File
@@ -0,0 +1,302 @@
/* sdk/meow_cachegov.c — 磁盘缓存 page-cache 治理 SDK 库(A:DONTNEED 主导侧)。
*
* 权威方案:devdocs/plan/OOM_PAGECACHE_GOVERNANCE.md。归属(ARCHITECTURE_PRINCIPLES ③混合):
* A 热路径(register/tick/drop_finished/refault)随 SDK 库;C(/proc/meminfo Cached 监控)
* 归 meow_svc_perf,本库只读其共享状态文件 MEOW_CACHEGOV_C_FILE 作为降载信号。
*
* 红线(务必保留本注释):**L2 禁止**——绝不丢 `[read_pos, ...)` 任何未解码区(含 read_pos 前方
* 刚写入尚未解码的页)。本库所有 drop 的上界只能是 read_pos-keep_hot(L1)或整文件曲终(L0)。
*
* 分层:只依赖 pthread + meow_log + POSIX(posix_fadvise),不依赖 core/App 其它 TU。
* 自含:refault 滚窗 + 三级回退(放大 keep_hot → 降 L0 → 关 A)。
*/
#include "include/meow_cachegov.h"
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
/* meow_ui.h:SDK 公共伞头(含 meow_log/meow_ui_log.h + 公共类型)。 */
#include "meow_ui.h"
/* 共享协议头:C 侧状态文件路径(meow_svc_perf 写入 / 各 App 读取,同源防漂移)。 */
#include "meow_ipc.h"
#define MEOW_CACHEGOV_MAXREG 8 /* 同时治理的缓存文件上限(ncm 单点播放,远小于此) */
#define MEOW_CACHEGOV_REFAULTRING 64 /* refault 滚窗环形缓冲 */
#define MEOW_CACHEGOV_WINDOW_MS 10000 /* refault 滚窗 10s */
#define MEOW_CACHEGOV_HOTESCALATE 1 /* 回退档>=1 时 keep_hot 放大倍数(翻倍) */
#define MEOW_CACHEGOV_DROP_THROTTLE (64u * 1024u) /* L1 每次至少推进 64KB 才再 drop(省 open/fadvise) */
typedef struct cg_entry {
char path[512];
uint64_t size; /* 标称大小(流式=nominal;命中=实际) */
uint64_t (*read_pos_fn)(void *ctx);
void *ctx;
uint64_t dropped_to; /* 已 DONTNEED 的已读前缀上界(只推进,不回退) */
int used;
} cg_entry_t;
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static struct meow_cachegov_cfg g_cfg = {
.enabled = 1,
.cached_threshold_kb = 0, /* C 归属 svc_perf,本库只读 */
.dirty_warn_kb = 0, /* 同上 */
.keep_hot_bytes = (256u * 1024u), /* 保 256KB 解码窗口供倒带 */
.min_hot_ahead = (0), /* L2 红线防御值;绝不参与 drop */
.drop_mode = 0, /* 默认仅曲终(L0);L1 需显式开(带 seek 回退) */
.refault_backoff = 3, /* 10s 内 3 次 refault → 回退 */
};
static cg_entry_t g_reg[MEOW_CACHEGOV_MAXREG];
static int g_backoff = 0; /* 0=正常 1=keep_hot放大 2=降L0 3=A关 */
static int64_t g_last_step_ms = -1000000000000LL; /* 上次升/降档时刻(初始很远 → 首次可动作) */
static int64_t g_refault_ring[MEOW_CACHEGOV_REFAULTRING];
static int g_refault_head = 0;
static int g_refault_n = 0;
/* ---- 时钟(宿主测试桩可注入) ---- */
static int64_t g_test_now_ms = -1;
static int64_t now_ms(void)
{
if (g_test_now_ms >= 0) return g_test_now_ms;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
void meow_cachegov_test_set_time_ms(int64_t t) { g_test_now_ms = t; }
/* ---- C 侧状态文件(svc_perf 写 / 本库读) ---- */
static const char *c_file(void)
{
const char *e = getenv("MEOW_CACHEGOV_C_FILE");
return (e && e[0]) ? e : MEOW_CACHEGOV_C_FILE;
}
/* C 状态(svc_perf 写)最近一次解析缓存:下载 cb 每 4KB 块会查一次,避免每块都读文件(200ms TTL)。 */
static int g_cached_c_dl = 0, g_cached_c_pf = 0;
static int64_t g_cached_c_at = 0;
/* 读 C 状态文件(缺省 0=不降载)。解析失败/文件不存在 → 全部 0。 */
static void read_c_state(int *dl_pause, int *pf_pause, long *cached_kb, long *dirty_kb)
{
if (dl_pause) *dl_pause = 0;
if (pf_pause) *pf_pause = 0;
if (cached_kb) *cached_kb = 0;
if (dirty_kb) *dirty_kb = 0;
FILE *f = fopen(c_file(), "r");
if (!f) return;
char line[160];
while (fgets(line, sizeof line, f)) {
if (!strncmp(line, "cached_kb=", 10)) { if (cached_kb) *cached_kb = atol(line + 10); }
else if (!strncmp(line, "dirty_kb=", 9)) { if (dirty_kb) *dirty_kb = atol(line + 9); }
else if (!strncmp(line, "dl_pause=", 9)) { if (dl_pause) *dl_pause = atol(line + 9) ? 1 : 0; }
else if (!strncmp(line, "prefetch_pause=", 15)) { if (pf_pause) *pf_pause = atol(line + 15) ? 1 : 0; }
}
fclose(f);
}
/* ---- refault 滚窗 ---- */
void meow_cachegov_note_refault(void)
{
int64_t t = now_ms();
pthread_mutex_lock(&g_mu);
g_refault_ring[g_refault_head] = t;
g_refault_head = (g_refault_head + 1) % MEOW_CACHEGOV_REFAULTRING;
if (g_refault_n < MEOW_CACHEGOV_REFAULTRING) g_refault_n++;
pthread_mutex_unlock(&g_mu);
}
static int refaults_last_window(void)
{
int64_t now = now_ms();
int cnt = 0, start = (g_refault_head - g_refault_n + MEOW_CACHEGOV_REFAULTRING) %
MEOW_CACHEGOV_REFAULTRING;
for (int i = 0; i < g_refault_n; i++) {
int64_t t = g_refault_ring[(start + i) % MEOW_CACHEGOV_REFAULTRING];
if (t >= now - MEOW_CACHEGOV_WINDOW_MS) cnt++;
}
return cnt;
}
/* 曲终/cur_finished 后:整文件 L0 DONTNEED。 */
int meow_cachegov_drop_finished(const char *path)
{
if (!path) return -1;
pthread_mutex_lock(&g_mu);
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++) {
if (g_reg[i].used && strcmp(g_reg[i].path, path) == 0) {
g_reg[i].used = 0;
break;
}
}
int lbf = g_backoff, en = g_cfg.enabled;
pthread_mutex_unlock(&g_mu);
if (!en || lbf >= 3) { /* A 已整体关闭:连曲终 L0 也不丢(§2.3 步骤 3) */
meow_log(MEOW_LOG_INFO, "[cachegov] drop_finished skip %s (A off, backoff=%d)", path, lbf);
return 0;
}
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
/* len=0 → 到 EOF(整文件)。曲终播放器已不再读该文件,重播=冷缓存首读(等价未命中)。 */
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
close(fd);
meow_log(MEOW_LOG_INFO, "[cachegov] drop_finished L0 %s", path);
return 0;
}
/* L1:丢已读前缀 [dropped_to, read_pos-keep_hot)。返回 1=有丢。 */
static int entry_drop_l1(cg_entry_t *e, size_t keep)
{
if (!e->read_pos_fn) return 0;
uint64_t pos = e->read_pos_fn(e->ctx);
if (pos <= keep) return 0; /* keep_hot 覆盖整个已读前缀,无需丢 */
uint64_t drop_end = pos - (uint64_t)keep; /* L1 上界:read_pos 前方绝不丢(L2 红线) */
if (drop_end <= e->dropped_to) return 0; /* 无推进(seek 回退 / 前进不足) */
if (drop_end - e->dropped_to < MEOW_CACHEGOV_DROP_THROTTLE && e->dropped_to != 0)
return 0; /* 节流:不足 64KB 不重复 open */
int fd = open(e->path, O_RDONLY);
if (fd < 0) return 0;
off_t off = (off_t)e->dropped_to;
off_t len = (off_t)(drop_end - e->dropped_to);
ssize_t r = posix_fadvise(fd, off, len, POSIX_FADV_DONTNEED);
close(fd);
if (r != 0) return 0;
e->dropped_to = drop_end;
meow_log(MEOW_LOG_INFO, "[cachegov] L1 drop %s off=%lld len=%lld (pos=%lld keep=%zu)",
e->path, (long long)off, (long long)len, (long long)pos, keep);
return 1;
}
int meow_cachegov_tick(void)
{
pthread_mutex_lock(&g_mu);
int64_t now = now_ms();
/* ① 回退评估(滚窗 10s):refault 超门限 → 升一档;滚窗干净 ≥10s → 降一档。
* 每升降一档需间隔 ≥10s(逐级回退,不因窗内持续 refault 一次跳过多档,防误回退)。 */
int cnt = refaults_last_window();
if (cnt >= g_cfg.refault_backoff && g_cfg.refault_backoff > 0 && g_backoff < 3 &&
(now - g_last_step_ms) >= MEOW_CACHEGOV_WINDOW_MS) {
g_backoff = g_backoff + 1;
g_last_step_ms = now;
meow_log(MEOW_LOG_INFO, "[cachegov] backoff -> %d (refault=%d/%lds)",
g_backoff, cnt, MEOW_CACHEGOV_WINDOW_MS / 1000);
} else if (cnt == 0 && g_backoff > 0 &&
(now - g_last_step_ms) >= MEOW_CACHEGOV_WINDOW_MS) {
g_backoff = g_backoff - 1;
g_last_step_ms = now;
meow_log(MEOW_LOG_INFO, "[cachegov] backoff -> %d (clean window)", g_backoff);
}
size_t keep = g_cfg.keep_hot_bytes;
if (g_backoff >= MEOW_CACHEGOV_HOTESCALATE) keep = g_cfg.keep_hot_bytes * 2; /* 档1:放大 keep_hot */
int l1_on = g_cfg.enabled && g_cfg.drop_mode >= 1 && g_backoff < 2; /* 档2:L1 关 */
int en = g_cfg.enabled;
/* ② L1 丢弃 */
int dropped = 0;
if (en && l1_on) {
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++)
if (g_reg[i].used) dropped += entry_drop_l1(&g_reg[i], keep);
}
pthread_mutex_unlock(&g_mu);
return dropped;
}
/* ---- 注册 / 注销 ---- */
int meow_cachegov_register(const char *path, uint64_t size,
uint64_t (*read_pos_fn)(void *ctx), void *ctx)
{
if (!path) return -1;
pthread_mutex_lock(&g_mu);
int slot = -1;
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++) {
if (g_reg[i].used && strcmp(g_reg[i].path, path) == 0) { slot = i; break; }
if (!g_reg[i].used && slot < 0) slot = i;
}
if (slot < 0) { pthread_mutex_unlock(&g_mu); return -1; }
snprintf(g_reg[slot].path, sizeof g_reg[slot].path, "%s", path);
g_reg[slot].size = size;
g_reg[slot].read_pos_fn = read_pos_fn;
g_reg[slot].ctx = ctx;
g_reg[slot].dropped_to = 0;
g_reg[slot].used = 1;
pthread_mutex_unlock(&g_mu);
meow_log(MEOW_LOG_INFO, "[cachegov] register %s size=%llu", path, (unsigned long long)size);
return 0;
}
int meow_cachegov_unregister(const char *path)
{
if (!path) return -1;
pthread_mutex_lock(&g_mu);
int rc = -1;
for (int i = 0; i < MEOW_CACHEGOV_MAXREG; i++)
if (g_reg[i].used && strcmp(g_reg[i].path, path) == 0) { g_reg[i].used = 0; rc = 0; break; }
pthread_mutex_unlock(&g_mu);
return rc;
}
/* ---- 降载查询(A 回退 OR svc_perf C) ---- */
static void c_state(int *dl_pause, int *pf_pause)
{
int64_t now = now_ms();
pthread_mutex_lock(&g_mu);
if (g_cached_c_at == 0 || (now - g_cached_c_at) > 200) {
read_c_state(&g_cached_c_dl, &g_cached_c_pf, NULL, NULL);
g_cached_c_at = now;
}
if (dl_pause) *dl_pause = g_cached_c_dl;
if (pf_pause) *pf_pause = g_cached_c_pf;
pthread_mutex_unlock(&g_mu);
}
int meow_cachegov_download_paused(void)
{
pthread_mutex_lock(&g_mu);
int a = (g_backoff >= 2);
pthread_mutex_unlock(&g_mu);
int c = 0;
c_state(&c, NULL);
return (a || c) ? 1 : 0;
}
int meow_cachegov_prefetch_paused(void)
{
pthread_mutex_lock(&g_mu);
int a = (g_backoff >= 3);
pthread_mutex_unlock(&g_mu);
int c = 0;
c_state(NULL, &c);
return (a || c) ? 1 : 0;
}
/* ---- 配置 ---- */
int meow_cachegov_set_cfg(const struct meow_cachegov_cfg *cfg)
{
if (!cfg) return -1;
pthread_mutex_lock(&g_mu);
g_cfg = *cfg;
pthread_mutex_unlock(&g_mu);
return 0;
}
int meow_cachegov_get_cfg(struct meow_cachegov_cfg *cfg)
{
if (!cfg) return -1;
pthread_mutex_lock(&g_mu);
*cfg = g_cfg;
pthread_mutex_unlock(&g_mu);
return 0;
}
int meow_cachegov_backoff_level(void)
{
pthread_mutex_lock(&g_mu);
int v = g_backoff;
pthread_mutex_unlock(&g_mu);
return v;
}
+151
View File
@@ -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;
}
+67
View File
@@ -0,0 +1,67 @@
/*
* meow_img_color.h — MeowOS SDK 封面双色渐变取色(批5)。
*
* 定位:从歌曲封面(已解码 RGBA 像素/meow_img 句柄)提取「主色 + 次色」,供播放页/歌词页动态主题
* (背景渐变/文字 accent)。纯 C 分桶统计,无 GPU、无浮点、无堆分配。
*
* 组织:独立公共头(仿 meow_lyric_view.h 模式,不随 meow_ui.h 伞头发布),取色原语是独立源码
* meow_img_color.c——**不并入 meow_ui.a**(铁律:避免并发重编/四 App 重链)。App/launcher 需显式
* `#include "meow_img_color.h"` 并在构建时单链 meow_img_color.c(宿主单测同法)。
*
* 约束(0 warning / 最小侵入):
* - meow_img 对外保持 opaque;像素直读经 SDK 正式访问器 `meow_img_pixels`(meow_ui_img.c §4,
* 已上收「局部补全结构体」遗留),宽高经 `meow_img_w`/`meow_img_h`。本 TU 不再局部补全布局。
* - 取色算法纯函数:meow_img_duocolor_rgba / meow_img_color_derive 纯 C、无 meow_* 依赖;
* meow_img_duocolor(句柄版)仅经上述三访问器读,不触碰 meow_ui.a 内部实现。宿主单测链
* meow_img_color.c + meow_ui_img.c 即可。
*
* 色彩语义:
* - 输入 RGBA(stb_image 输出,4 通道 straight alpha),w×h,最多遍历 ~3.6k 采样(内部自适
* 副采样步进,等效缩略图成本,避免全尺寸逐像素)。
* - 色域分桶 4×4×4=64(RGB 各 4 段),跳过近黑(luma<24)/近白(luma>244)/透明(alpha<64),
* 按「像素数 + 饱和度」加权;主色=加权最大桶中心;次色=与主色色距最大的显著桶(≥主桶 14%)
* 中心;无显著桶→次色=主色缩暗 2/3(渐变互补)。
* - 返回 0=成功;-1=参数非法 / 有效采样不足(调用方回退全局 meow_theme)。
*/
#ifndef MEOW_IMG_COLOR_H
#define MEOW_IMG_COLOR_H
#include "meow_ui.h" /* meow_img 句柄 + meow_theme */
#include "meow_lyric_view.h" /* meow_lyric_view_colors(派生歌词页覆盖) */
#ifdef __cplusplus
extern "C" {
#endif
/* 从 RGBA 像素缓冲取主色+次色(纯算法;样本副采样)。返回 0=成功;-1=失败。 */
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
uint32_t *main, uint32_t *second);
/* 从 meow_img 句柄取主色+次色(经 meow_img_pixels 读像素,见 .c)。返回 0=成功;-1=失败。 */
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second);
/* 由主/次色派生播放页 + 歌词页封面主题(纯函数,可宿主单测)。
* th 用于默认回退色(main/second 为 0 时);调用方应传 meow_theme_get()。
* 派生语义:
* - 背景渐变 = 主色(顶)→ 次色(底)各压暗 55%(防刺眼 + 保文字可读);
* - fg = 可读文字色(按主次平均亮度选黑/白),fg_dim = 向背景靠拢的次要文字色;
* - accent = 次色(与主色底对比),accent_fg = accent 上可读图标色;
* - lyr = 歌词页 set_colors 覆盖:scrim=主色压暗(alpha 176)、cur=fg,gray 留 0(由 lyric_view
* 以「cur 为基准自动递减」——批5 已修正对覆盖 cur 亦然)。 */
typedef struct meow_img_color_theme {
uint32_t bg_top; /* 播放页渐变顶 */
uint32_t bg_bottom; /* 播放页渐变底 */
uint32_t fg; /* 主要文字色(背景上可读) */
uint32_t fg_dim; /* 次要文字色 */
uint32_t accent; /* 强调色(控件/进度/封面占位) */
uint32_t accent_fg; /* accent 上可读前景(图标) */
meow_lyric_view_colors lyr; /* 歌词页 set_colors 覆盖 */
} meow_img_color_theme;
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
meow_img_color_theme *out);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_IMG_COLOR_H */
+1005
View File
File diff suppressed because it is too large. Load diff
+50
View File
@@ -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 */
+361
View File
@@ -0,0 +1,361 @@
/*
* meow_lazygrid.c — §12.4 2 维虚拟化宫格(M7.4 主界面前置)。
*
* 自包含控件(非 meow_widget,不注册 z 序/绘制注册表):宿主在 draw 阶段调 meow_lazygrid_draw、
* 命中阶段调 meow_lazygrid_hit/tap_at、滚动喂 meow_scroll(meow_lazygrid_scroll 返回会话)。
* 只实例化**可见行**:固定 item 池(视口行数 + 2 × cols,静态有界 ≤MEOW_LAZYGRID_POOL_MAX),
* bind 仅在 item 进入视口时调一次(回滚同槽不重绑,同 listview 池语义)。
*
* 内存纪律:池在 create/set_vsource/set_cell(冷路径)分配;draw/hit/set_scroll 热路径零 malloc。
* 颜色全取 meow_theme_get() 语义色(日/夜/style 自动),不硬编码。
*/
#include "meow_ui.h"
#include "include/meow_lazygrid.h"
#include "meow_ui_internal.h" /* meow_gfx_clip_push/pop + meow_gfx_fill_round */
#include "include/meow_ui_scroll.h"
#include <string.h>
#include <stdlib.h>
struct meow_lazygrid {
int x, y, w, h; /* 视口矩形(绝对坐标) */
int cols; /* 列数 */
int cell_w, cell_h, gap; /* 格宽/格高/间距 */
int row_h; /* 行高 = cell_h + gap(缓存) */
int scroll; /* 滚动偏移(像素) */
int sel; /* 选中项(-1=无) */
meow_lazygrid_count_fn count_fn; void *vctx;
meow_lazygrid_bind_fn bind;
meow_lazygrid_draw_fn draw_cb; void *draw_ctx;
meow_lazygrid_tap_fn tap_cb; void *tap_ctx;
/* 固定 item 池(冷路径分配;slot 按 item index mod pool_n 映射) */
meow_lazygrid_item *pool;
int *pool_idx; /* 每槽绑定的 item index(-1=空闲) */
int pool_n;
char empty_text[64]; /* 空态占位(首字符 0=不画) */
};
/* 行高(= cell_h + gap)。 */
int meow_lazygrid_row_h(const meow_lazygrid *g)
{ return g ? g->row_h : 0; }
/* 总行数。 */
static int lg_rows(const meow_lazygrid *g)
{
int n = g->count_fn ? g->count_fn(g->vctx) : 0;
if (n < 0) n = 0;
if (n == 0) return 0;
return (n + g->cols - 1) / g->cols;
}
/* 总项数。 */
static int lg_count(const meow_lazygrid *g)
{ return g->count_fn ? g->count_fn(g->vctx) : 0; }
/* 内容总高(行高 × 行数 − 末行后无 gap)。 */
static int lg_content_h(const meow_lazygrid *g)
{
int rows = lg_rows(g);
if (rows <= 0) return 0;
return rows * g->cell_h + (rows - 1) * g->gap;
}
/* ---- 生命周期 ---- */
meow_lazygrid *meow_lazygrid_create(int x, int y, int w, int h, int cols,
int cell_w, int cell_h, int gap)
{
if (cols < 1) cols = 1;
meow_lazygrid *g = (meow_lazygrid *)calloc(1, sizeof *g);
if (!g) return NULL;
g->x = x; g->y = y; g->w = w; g->h = h; g->cols = cols;
g->cell_w = cell_w > 0 ? cell_w : 1;
g->cell_h = cell_h > 0 ? cell_h : 1;
g->gap = gap > 0 ? gap : 0;
g->row_h = g->cell_h + g->gap;
g->sel = -1;
return g;
}
void meow_lazygrid_free(meow_lazygrid *g)
{
if (!g) return;
free(g->pool); free(g->pool_idx);
free(g);
}
void meow_lazygrid_set_rect(meow_lazygrid *g, int x, int y, int w, int h)
{
if (!g) return;
g->x = x; g->y = y; g->w = w; g->h = h;
}
void meow_lazygrid_set_cols(meow_lazygrid *g, int cols)
{
if (!g) return;
if (cols < 1) cols = 1;
if (g->cols == cols) return;
g->cols = cols;
}
void meow_lazygrid_set_cell(meow_lazygrid *g, int cell_w, int cell_h, int gap)
{
if (!g) return;
g->cell_w = cell_w > 0 ? cell_w : 1;
g->cell_h = cell_h > 0 ? cell_h : 1;
g->gap = gap > 0 ? gap : 0;
g->row_h = g->cell_h + g->gap; /* 行高变化 → 可见行数变 → 池容量随之调整 */
}
/* ---- item 池(冷路径;有界 ≤MEOW_LAZYGRID_POOL_MAX)---- */
static int lg_pool_cap(const meow_lazygrid *g)
{
int rh = g->row_h; if (rh < 8) rh = 8;
int rows = (g->h / rh) + 1; if (rows < 1) rows = 1;
int cap = (rows + 2) * g->cols;
if (cap < 8) cap = 8;
if (cap > MEOW_LAZYGRID_POOL_MAX) cap = MEOW_LAZYGRID_POOL_MAX;
return cap;
}
/* 确保池容量 ≥ cap(有界扩张;重分配清空绑定 → 下一帧按需重绑)。返回 0=成功;-1=分配失败。 */
static int lg_ensure_pool(meow_lazygrid *g)
{
if (!g->count_fn || !g->bind) return 0;
int cap = lg_pool_cap(g);
if (g->pool && g->pool_n >= cap) return 0;
meow_lazygrid_item *np = (meow_lazygrid_item *)calloc((size_t)cap, sizeof *np);
int *ni = (int *)calloc((size_t)cap, sizeof *ni);
if (!np || !ni) { free(np); free(ni); return -1; }
for (int i = 0; i < cap; i++) ni[i] = -1;
free(g->pool); free(g->pool_idx);
g->pool = np; g->pool_idx = ni; g->pool_n = cap;
return 0;
}
/* 取 index 对应的池槽(按 item index mod pool_n 映射;槽绑定不匹配 → 防御性重绑)。 */
static meow_lazygrid_item *lg_slot(meow_lazygrid *g, int index)
{
if (!g->pool || g->pool_n <= 0) return NULL;
int s = (int)((unsigned)index % (unsigned)g->pool_n);
if (g->pool_idx[s] != index) {
memset(&g->pool[s], 0, sizeof g->pool[s]);
g->bind(index, &g->pool[s], g->vctx);
g->pool_idx[s] = index;
}
return &g->pool[s];
}
/* ---- 数据源 + 渲染 ---- */
int meow_lazygrid_set_vsource(meow_lazygrid *g, meow_lazygrid_count_fn count,
meow_lazygrid_bind_fn bind, void *ctx)
{
if (!g || !count || !bind) return -1;
g->count_fn = count; g->bind = bind; g->vctx = ctx;
if (lg_ensure_pool(g) != 0) { /* 分配固定 item 池(有界,冷路径) */
g->count_fn = NULL; g->bind = NULL; /* 池分配失败:回退为空 */
return -1;
}
return 0;
}
int meow_lazygrid_set_item_draw(meow_lazygrid *g, meow_lazygrid_draw_fn draw,
void *ctx)
{
if (!g) return -1;
g->draw_cb = draw; g->draw_ctx = ctx; /* NULL=用默认卡片 renderer */
return 0;
}
int meow_lazygrid_set_empty_text(meow_lazygrid *g, const char *text)
{
if (!g) return -1;
if (text) {
strncpy(g->empty_text, text, sizeof g->empty_text - 1);
g->empty_text[sizeof g->empty_text - 1] = '\0';
} else {
g->empty_text[0] = '\0';
}
return 0;
}
/* ---- 滚动 ---- */
int meow_lazygrid_max_scroll(meow_lazygrid *g)
{
if (!g) return 0;
int ch = lg_content_h(g);
int ms = ch - g->h;
return ms > 0 ? ms : 0;
}
void meow_lazygrid_set_scroll(meow_lazygrid *g, int off)
{
if (!g) return;
int ms = meow_lazygrid_max_scroll(g);
if (off < 0) off = 0;
if (off > ms) off = ms;
g->scroll = off;
}
void meow_lazygrid_scroll_by(meow_lazygrid *g, int dy)
{ if (g) meow_lazygrid_set_scroll(g, g->scroll + dy); }
int meow_lazygrid_scroll(meow_lazygrid *g) { return g ? g->scroll : 0; }
/* ---- meow_scroll 集成(callbacks 指回本宫格)---- */
static void lg_s_set(void *c, int off) { meow_lazygrid_set_scroll((meow_lazygrid *)c, off); }
static int lg_s_max(void *c) { return meow_lazygrid_max_scroll((meow_lazygrid *)c); }
static int lg_s_get(void *c) { return meow_lazygrid_scroll((meow_lazygrid *)c); }
static void lg_s_redraw(void *c)
{
meow_lazygrid *g = (meow_lazygrid *)c;
if (g && g->w > 0 && g->h > 0) {
meow_lazygrid_draw(g, meow_theme_get());
meow_ui_flush_region(g->x, g->y, g->w, g->h); /* 只刷视口区 */
}
}
meow_scroll *meow_lazygrid_scroll_attach(meow_lazygrid *g)
{
if (!g) return NULL;
return meow_scroll_create(lg_s_set, lg_s_max, lg_s_get, lg_s_redraw, g);
}
/* ---- 选中 ---- */
void meow_lazygrid_set_sel(meow_lazygrid *g, int idx)
{
if (!g) return;
int n = lg_count(g);
if (idx < 0) { g->sel = -1; return; }
if (n > 0 && idx >= n) idx = n - 1;
g->sel = idx;
}
int meow_lazygrid_sel(const meow_lazygrid *g) { return g ? g->sel : -1; }
void meow_lazygrid_set_on_tap(meow_lazygrid *g, meow_lazygrid_tap_fn cb, void *ctx)
{
if (!g) return;
g->tap_cb = cb; g->tap_ctx = ctx;
}
int meow_lazygrid_tap_at(meow_lazygrid *g, int x, int y)
{
int idx = meow_lazygrid_hit(g, x, y);
if (idx < 0) return -1;
meow_lazygrid_set_sel(g, idx);
if (g->tap_cb) g->tap_cb(idx, g->tap_ctx);
return idx;
}
/* ---- 命中(滚动 + 网格坐标 → item idx)----
* 点在格内(跨过 gap 区域返回 -1);禁用格不响应(同 listview enabled=0 语义)。 */
int meow_lazygrid_hit(meow_lazygrid *g, int x, int y)
{
if (!g) return -1;
if (x < g->x || x >= g->x + g->w || y < g->y || y >= g->y + g->h) return -1;
/* 列格(水平):col = px / (cell_w+gap),落点在 gap 内 → 未命中。 */
int px = x - g->x;
int cstep = g->cell_w + g->gap;
int col = px / cstep;
if (col < 0 || col >= g->cols) return -1;
if ((px - col * cstep) >= g->cell_w) return -1;
/* 行格(垂直,含滚动):row = (py+scroll)/row_h;落点在 gap 内 → 未命中。 */
int py = y - g->y + g->scroll;
int rstep = g->row_h;
int row = py / rstep;
if (row < 0) return -1;
if ((py - row * rstep) >= g->cell_h) return -1;
int idx = row * g->cols + col;
int n = lg_count(g);
if (idx < 0 || idx >= n) return -1;
/* 禁用格不响应(顺手 bind 临时 enabled 查:与 listview hit 同法)。 */
if (g->bind) {
meow_lazygrid_item it;
memset(&it, 0, sizeof it);
g->bind(idx, &it, g->vctx);
if (!it.enabled) return -1;
}
return idx;
}
/* ---- 绘制(只画可见行;clip 钳到宫格视口)---- */
/* MD3 容器(默认 renderer P0):格=格内对称内缩 surfaceContainer 圆角卡(radius_card)+ 卡内 tonal
* 选中(primaryContainer)/ 按压(primary α26);画布=整格(触控目标),卡在其内对称内缩(同
* meow_listview 行>卡范式:卡片外仍命中整格,视觉卡留出呼吸空隙)。完全自绘(set_item_draw)
* 覆盖本 renderer,App 自理容器观感。 */
#define MEOW_LAZYGRID_CARD_MARGIN 8 /* 格内对称边距(卡内缩;与 cell gap 叠加出卡间空隙) */
static void lg_draw_cell(meow_lazygrid *g, const meow_theme *th, int index,
int cell_x, int cell_y, meow_lazygrid_item *it)
{
int sel = (index == g->sel);
int txtcol = sel ? th->onPrimaryContainer : th->onSurface;
int subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
int m = MEOW_LAZYGRID_CARD_MARGIN;
int cw = g->cell_w - 2 * m, chh = g->cell_h - 2 * m; /* 卡尺寸(格内对称内缩) */
int cx = cell_x + m, cy = cell_y + m; /* 卡左上 */
int press = meow_input_touch_down() &&
meow_input_touch_x() >= cell_x && meow_input_touch_x() < cell_x + g->cell_w &&
meow_input_touch_y() >= cell_y && meow_input_touch_y() < cell_y + g->cell_h;
meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->surfaceContainer);
if (sel) meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, chh, th->radius_card, th->primary, 26);
int pad = 8;
int thh = meow_ui_text_height(th->font_body);
/* 图标(可选):居中于卡顶 ~50%(卡中心=格中心,因对称内缩)。 */
if (it->icon) {
int isz = g->cell_w * 6 / 10; if (isz > g->cell_h * 5 / 10) isz = g->cell_h * 5 / 10;
if (isz < 16) isz = 16;
int icx = cx + cw / 2, icy = cy + pad + isz / 2;
meow_ui_img_scaled(icx - isz / 2, icy - isz / 2, isz, isz, (meow_img *)it->icon);
}
/* 标题 + 副标题(主界面:名 + 状态)。卡内底部对齐。 */
int has_sub = (it->subtitle && it->subtitle[0]);
int ty = cy + chh - pad - thh - (has_sub ? (thh + 2) : 0);
if (it->title) meow_ui_text_center(cx + cw / 2, ty, it->title, th->font_body, txtcol);
if (has_sub) meow_ui_text_center(cx + cw / 2, ty + thh + 2, it->subtitle, th->font_body, subcol);
/* 选中描边(轮廓);禁用卡内变淡(圆角 α,防圆角外叠出灰方块)。 */
if (sel) meow_ui_draw_rect_round(cx, cy, cw, chh, th->radius_card, th->primary);
if (!it->enabled)
meow_gfx_fill_round_alpha(cx, cy, cw, chh, th->radius_card, th->onSurface, 90);
}
void meow_lazygrid_draw(meow_lazygrid *g, const meow_theme *th)
{
if (!g || !th) return;
meow_gfx_clip_push(g->x, g->y, g->w, g->h); /* 格画不出宫格视口(防泄漏) */
int n = lg_count(g);
int rows = lg_rows(g);
lg_ensure_pool(g); /* 防御:视口/列几何变化时确保池容量 */
/* 空态占位:count==0 且设 empty_text → 视口内居中。 */
if (n == 0 && g->empty_text[0]) {
int thh = meow_ui_text_height(th->font_body);
meow_ui_text_center(g->x + g->w / 2, g->y + (g->h - thh) / 2, g->empty_text,
th->font_body, th->onSurfaceVariant);
meow_gfx_clip_pop();
return;
}
int off = g->scroll;
int cstep = g->cell_w + g->gap;
int rstep = g->row_h;
if (rstep <= 0) rstep = 1;
int first = off / rstep;
int last = (off + g->h) / rstep + 1; if (last > rows) last = rows;
if (first < 0) first = 0;
if (last > rows) last = rows;
for (int r = first; r < last; r++) {
int ry = g->y + (r * rstep - off); /* 本行格顶视口坐标 */
for (int c = 0; c < g->cols; c++) {
int index = r * g->cols + c;
if (index >= n) break; /* 末行余下空格 */
int cx = g->x + c * cstep;
/* 行完全滚出视口(裁剪由 clip 兜底,这里跳过整行) */
if (ry >= g->y + g->h) break;
if (ry + g->cell_h <= g->y) continue;
if (g->draw_cb) {
int st = (index == g->sel) ? MEOW_LAZYGRID_ST_SELECTED : 0;
g->draw_cb(index, cx, ry, g->cell_w, g->cell_h, st, g->draw_ctx);
} else {
meow_lazygrid_item *it = lg_slot(g, index);
if (!it) continue;
lg_draw_cell(g, th, index, cx, ry, it);
}
}
}
meow_gfx_clip_pop();
}
+314
View File
@@ -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;
}
+82
View File
@@ -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 */
+362
View File
@@ -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;
}
+110
View File
@@ -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 */
+157
View File
@@ -0,0 +1,157 @@
/*
* meow_net_ipc.c — MeowOS SDK 网络自愿通道 → netmgrd IPC 桥(期4;追加式协议 0x00EE/0x00EF)。
*
* 与 meow_notify_ipc.c 同型:只做「一次性 connect + send + read」,不维护连接/订阅循环。
* - meow_net_policy_check(host):MSG_NET_SDK op=NET_SDK_CHECK(app_id=MEOW_LOG_APP + host);
* 读到 allow=1 → 返回 0(allow),allow=0 → 返回 1(block);任何失败(无 socket/超时/非法应答)
* → 返回 0(乐观放行,绝不因监控不可用误封业务——NETMGR_DESIGN §6.3 自愿性质)。
* - meow_net_report(rx,tx):MSG_NET_SDK op=NET_SDK_REPORT;send 后读一次应答头即关(不读文本),
* best-effort,失败静默(返回值 void)。
* 映射层(SDK 入参 ↔ IPC m_net_sdk_t 字段)集中在本文件;app_id 取 env MEOW_LOG_APP(与 meow_ui_log.c
* 口径一致),socket 路径取 env MEOW_NET_SOCK(宿主/qemu 覆盖)缺省 /usr/data/meow_net.sock。
*
* 线程安全:app_id pthread_once 缓存;本文件纯传输层不调 meow_log(与 meow_notify_ipc.c 同约定)。
*/
#include "meow_net_ipc.h"
#include "meow_ipc.h" /* 共享协议:帧头/m_net_sdk_t/m_net_sdk_result_t/MSG_NET_SDK* —— 防漂移 */
#include <stdio.h> /* snprintf */
#include <stdlib.h> /* getenv */
#include <string.h>
#include <errno.h> /* EINTR */
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#define MEOW_NET_IPC_TIMEOUT_MS 500 /* 应答读超时:netmgrd 单线程 poll,>500ms 视为降级 */
static pthread_once_t g_once = PTHREAD_ONCE_INIT;
static char g_app_id[MEOW_APP_ID_MAX] = "app";
static void ipc_init(void)
{
const char *s = getenv("MEOW_LOG_APP");
if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s);
}
const char *meow_net_ipc_app_id(void)
{
pthread_once(&g_once, ipc_init);
return g_app_id;
}
/* socket 路径:优先 env MEOW_NET_SOCK,缺省 /usr/data/meow_net.sock。 */
static const char *ipc_sock_path(void)
{
const char *s = getenv("MEOW_NET_SOCK");
return (s && s[0]) ? s : "/usr/data/meow_net.sock";
}
/* 连接 socket;失败 -1(调用方降级)。 */
static int conn_path(const char *path)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
return fd;
}
static int write_all(int fd, const uint8_t *p, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, p + off, n - off);
if (w < 0) { if (errno == EINTR) continue; return 0; }
if (w == 0) return 0;
off += (size_t)w;
}
return 1;
}
static int read_all(int fd, uint8_t *p, size_t n)
{
size_t off = 0;
while (off < n) {
ssize_t r = read(fd, p + off, n - off);
if (r < 0) { if (errno == EINTR) continue; return 0; }
if (r == 0) return 0;
off += (size_t)r;
}
return 1;
}
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rl = len;
uint16_t ver = MEOW_IPC_VERSION, t = type;
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2);
memcpy(hdr + 8, &req_id, 4); memcpy(hdr + 12, &rl, 4);
if (!write_all(fd, hdr, sizeof hdr)) return 0;
if (len && payload && !write_all(fd, payload, len)) return 0;
return 1;
}
static void set_timeout(int fd)
{
struct timeval tv;
tv.tv_sec = MEOW_NET_IPC_TIMEOUT_MS / 1000;
tv.tv_usec = (MEOW_NET_IPC_TIMEOUT_MS % 1000) * 1000;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
}
int meow_net_policy_check(const char *host)
{
if (!host || !host[0]) return 0;
m_net_sdk_t q;
memset(&q, 0, sizeof q);
q.op = NET_SDK_CHECK;
snprintf(q.app_id, sizeof q.app_id, "%s", meow_net_ipc_app_id());
snprintf(q.host, sizeof q.host, "%s", host);
int fd = conn_path(ipc_sock_path());
if (fd < 0) return 0; /* java socket:乐观放行 */
set_timeout(fd);
int allow = 0; /* 默认放行 */
if (send_frame(fd, MSG_NET_SDK, 0x4E455443u /* "NETC" */, &q, (uint32_t)sizeof q)) {
uint8_t rhdr[MEOW_IPC_HDR_LEN];
if (read_all(fd, rhdr, sizeof rhdr)) {
uint16_t rtype; uint32_t rlen;
memcpy(&rtype, rhdr + 6, 2); memcpy(&rlen, rhdr + 12, 4);
if (rtype == MSG_NET_SDK_RESULT && rlen >= 10) {
uint8_t pay[12];
if (read_all(fd, pay, 12)) { /* code(4)+allow(4)+text_len(2)+pad(2) */
m_net_sdk_result_t res;
memset(&res, 0, sizeof res);
memcpy(&res.code, pay, 4);
memcpy(&res.allow, pay + 4, 4);
memcpy(&res.text_len, pay + 8, 2);
if (res.text_len < sizeof res.text)
read_all(fd, (uint8_t *)res.text, res.text_len);
if (res.code == MEOW_OK) allow = (res.allow != 0) ? 1 : 0;
}
}
}
}
close(fd);
return allow ? 0 : 1; /* allow=1 → 返回 0(放行);allow=0 → 返回 1(block) */
}
void meow_net_report(uint32_t rx, uint32_t tx)
{
m_net_sdk_t q;
memset(&q, 0, sizeof q);
q.op = NET_SDK_REPORT;
snprintf(q.app_id, sizeof q.app_id, "%s", meow_net_ipc_app_id());
q.rx = rx; q.tx = tx;
int fd = conn_path(ipc_sock_path());
if (fd < 0) return; /* 无 socket:静默 */
set_timeout(fd);
if (send_frame(fd, MSG_NET_SDK, 0x4E455452u /* "NETR" */, &q, (uint32_t)sizeof q)) {
uint8_t rhdr[MEOW_IPC_HDR_LEN];
read_all(fd, rhdr, sizeof rhdr); /* best-effort:读一次应答头即关,不解析 */
}
close(fd);
}
+28
View File
@@ -0,0 +1,28 @@
/*
* meow_net_ipc.h — MeowOS SDK 网络自愿通道 → netmgrd IPC 桥(内部头,不随 meow_ui.h 发布)。
*
* 定位(NETMGR_DESIGN §6.3/§4):让框架 App 的 meow_net_policy_check()/meow_net_report() 真正经
* AF_UNIX IPC 到 netmgrd(/usr/data/meow_net.sock)。协议帧与负载结构复用共享 meow_ipc.h:
* - MSG_NET_SDK(0x00EE) ↑:op=NET_SDK_CHECK(app_id+host)/ NET_SDK_REPORT(app_id+rx+tx);
* - MSG_NET_SDK_RESULT(0x00EF) ↓:code + (CHECK)allow + 状态文本。
* 约定:只做「一次性 connect + send + read」,不维护订阅循环(同 meow_notify_ipc.c 型);
* 全部失败路径优雅降级(无 socket/写失败/超时/非法应答 → check 返回 0 放行、report 静默)。
*/
#ifndef MEOW_NET_IPC_H
#define MEOW_NET_IPC_H
#include <stdint.h>
#include "meow_ui.h" /* meow_net_policy_check / meow_net_report */
#ifdef __cplusplus
extern "C" {
#endif
/* 当前 app 的 app_id(env MEOW_LOG_APP,缺省 "app";pthread_once 缓存,线程安全)。 */
const char *meow_net_ipc_app_id(void);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_NET_IPC_H */
+499
View File
@@ -0,0 +1,499 @@
/*
* meow_png_stream.c — M23 真缩解后端(二):PNG 逐行 inflate + BMP 按行跳采样。
*
* 为什么不用 stb 的 PNG 路径:stb 先整幅 inflate + 反滤波成 raw,再展开成 RGBA 整幅,
* 最后才降采样——1600×2400 PNG 实测 +26.2MB。这里改成「按行流式」:
* - IDAT 边到边 inflate,一次只持有一行(filter 字节 + rowbytes)与上一行;
* - 反滤波后**只把需要的列**写进目标行(最近邻,与 meow_img_downscale 同口径);
* - 不需要的行整行丢弃(但仍要反滤波,filter 依赖上一行);
* - 峰值 ≈ 两行缓冲 + zlib 窗口(32KB) + 目标 RGBA,与源图分辨率无关。
* 支持:gray / gray+A / RGB / RGBA / 调色板,bit depth 1/2/4/8/16(16bit 取高字节降 8bit),
* tRNS 透明。不支持 → 返回 0 回退 stb:Adam7 交错、非 0 压缩/滤波方法、异常色型组合。
*
* BMP:文件流按目标行直接 seek 到源行(不读全图),行内跳列采样。支持 8bpp 调色板 /
* 16bpp RGB555 / 24bpp / 32bpp(BI_RGB);RLE、BITFIELDS、1/2/4bpp → 回退 stb。
*
* 许可证:MeowOS 自写(MIT,随 SDK 发布)。PNG/zlib 语义参考 RFC 2083 / zlib 手册;
* BMP 布局参考 stb_image 的解析口径(同为本仓已有依赖,行为对齐)。
*/
#include "meow_jpeg.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ---------------- zlib ABI ---------------- */
#if defined(__has_include)
# if __has_include(<zlib.h>)
# include <zlib.h>
# define MZP_HAVE_H 1
# endif
#endif
#ifndef MZP_HAVE_H
/* 设备 musl sysroot 没有 zlib.h(meow_font.c 同此口径):按 libz.a 的 ABI 声明最小 API。
* 指针/unsigned long 交给编译器定尺寸,宿主 64 位与设备 32 位都成立。 */
typedef unsigned char mzp_Bytef;
typedef unsigned int mzp_uInt;
typedef unsigned long mzp_uLong;
typedef void *mzp_voidpf;
typedef struct mzp_stream_s {
mzp_Bytef *next_in; mzp_uInt avail_in; mzp_uLong total_in;
mzp_Bytef *next_out; mzp_uInt avail_out; mzp_uLong total_out;
char *msg;
void *state;
void *zalloc;
void *zfree;
mzp_voidpf opaque;
int data_type;
mzp_uLong adler;
mzp_uLong reserved;
} mzp_stream;
extern int inflateInit_(mzp_stream *, const char *, int);
extern int inflate(mzp_stream *, int);
extern int inflateEnd(mzp_stream *);
extern const char *zlibVersion(void);
# define z_stream mzp_stream
# define Z_OK 0
# define Z_STREAM_END 1
# define Z_NO_FLUSH 0
#endif
/* ---------------- 字节源 ---------------- */
typedef struct {
FILE *f;
const unsigned char *mem;
size_t memlen, mempos;
} png_src;
static int ps_read(png_src *s, void *dst, size_t n)
{
if (s->mem) {
if (s->mempos + n > s->memlen) return 0;
memcpy(dst, s->mem + s->mempos, n);
s->mempos += n;
return 1;
}
return fread(dst, 1, n, s->f) == n;
}
static int ps_skip(png_src *s, size_t n)
{
if (s->mem) {
if (s->mempos + n > s->memlen) return 0;
s->mempos += n;
return 1;
}
return fseek(s->f, (long)n, SEEK_CUR) == 0;
}
/* ================================================================== */
/* PNG */
/* ================================================================== */
typedef struct {
png_src src;
z_stream z;
int z_ready, z_end;
unsigned w, h;
int depth, color;
int bits; /* 每像素位数 */
size_t rowbytes;
unsigned char *line; /* 1 filter 字节 + rowbytes */
unsigned char *prev; /* 上一行的像素字节 */
unsigned char *cur; /* 本行的像素字节 */
size_t need, got;
unsigned char pal[256][3];
unsigned char trns_a[256]; /* 调色板透明度 */
int pal_n;
int have_trns;
unsigned trns[3]; /* gray / RGB 的 tRNS 值(已降到 8bit) */
unsigned char *out;
int tw, th;
int next_ty;
unsigned y;
} png_ctx;
/* 从行里取 px 列的 8bit 通道;返回通道数(写入 o[0..3] 的 RGB(A) 语义由调用方定)。 */
static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsigned char *o)
{
int d = c->depth;
if (d < 8) {
int per = 8 / d; /* 每字节像素数 */
int byte = x / per;
int shift = 8 - d * (x % per + 1);
int v = (row[byte] >> shift) & ((1 << d) - 1);
if (c->color == 3) { /* 调色板:子字节值即索引 */
o[0] = c->pal[v][0]; o[1] = c->pal[v][1]; o[2] = c->pal[v][2];
o[3] = c->trns_a[v];
} else {
unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1));
o[0] = o[1] = o[2] = g;
/* tRNS 存的是原始采样值(0..2^d-1),不是展开后的 0..255 */
o[3] = (c->color == 0 && c->have_trns && (unsigned)v == c->trns[0]) ? 0 : 255;
}
return;
}
if (d == 8) {
if (c->color == 0) { /* gray */
o[0] = o[1] = o[2] = row[x];
o[3] = (c->have_trns && row[x] == c->trns[0]) ? 0 : 255;
} else if (c->color == 2) { /* RGB */
o[0] = row[x * 3]; o[1] = row[x * 3 + 1]; o[2] = row[x * 3 + 2];
o[3] = (c->have_trns && row[x * 3] == c->trns[0] &&
row[x * 3 + 1] == c->trns[1] && row[x * 3 + 2] == c->trns[2]) ? 0 : 255;
} else if (c->color == 3) { /* palette */
int i = row[x];
o[0] = c->pal[i][0]; o[1] = c->pal[i][1]; o[2] = c->pal[i][2];
o[3] = c->trns_a[i];
} else if (c->color == 4) { /* gray + A */
o[0] = o[1] = o[2] = row[x * 2];
o[3] = row[x * 2 + 1];
} else { /* RGBA */
o[0] = row[x * 4]; o[1] = row[x * 4 + 1];
o[2] = row[x * 4 + 2]; o[3] = row[x * 4 + 3];
}
return;
}
/* 16bit:取高字节 */
if (c->color == 0) {
o[0] = o[1] = o[2] = row[x * 2];
o[3] = (c->have_trns && row[x * 2] == c->trns[0]) ? 0 : 255;
} else if (c->color == 2) {
o[0] = row[x * 6]; o[1] = row[x * 6 + 2]; o[2] = row[x * 6 + 4];
o[3] = (c->have_trns && row[x * 6] == c->trns[0] &&
row[x * 6 + 2] == c->trns[1] && row[x * 6 + 4] == c->trns[2]) ? 0 : 255;
} else if (c->color == 4) {
o[0] = o[1] = o[2] = row[x * 4];
o[3] = row[x * 4 + 2];
} else {
o[0] = row[x * 8]; o[1] = row[x * 8 + 2];
o[2] = row[x * 8 + 4]; o[3] = row[x * 8 + 6];
}
}
/* 一行反滤波完成后:按需写目标行。 */
static int png_emit_row(png_ctx *c)
{
unsigned char ft = c->line[0];
const unsigned char *src = c->line + 1;
unsigned char *dst = c->cur;
size_t x, bpp = (size_t)((c->bits + 7) / 8);
if (bpp < 1) bpp = 1;
if (ft > 4) return 0;
for (x = 0; x < c->rowbytes; x++) {
unsigned char left = (x >= bpp) ? dst[x - bpp] : 0;
unsigned char up = c->prev ? c->prev[x] : 0;
unsigned char ul = (c->prev && x >= bpp) ? c->prev[x - bpp] : 0;
unsigned char v = src[x];
switch (ft) {
case 0: dst[x] = v; break;
case 1: dst[x] = (unsigned char)(v + left); break;
case 2: dst[x] = (unsigned char)(v + up); break;
case 3: dst[x] = (unsigned char)(v + (((unsigned)left + (unsigned)up) >> 1)); break;
default: {
int p = (int)left + (int)up - (int)ul;
int pa = p - (int)left; if (pa < 0) pa = -pa;
int pb = p - (int)up; if (pb < 0) pb = -pb;
int pc = p - (int)ul; if (pc < 0) pc = -pc;
unsigned char pred = (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : ul);
dst[x] = (unsigned char)(v + pred);
break;
}
}
}
if (c->next_ty < c->th) {
long sy = (long)c->next_ty * (long)c->h / c->th;
if (sy == (long)c->y) {
unsigned char *o = c->out + (size_t)c->next_ty * c->tw * 4;
int tx;
for (tx = 0; tx < c->tw; tx++) {
int sx = (int)((long)tx * (long)c->w / c->tw);
unsigned char px[4];
png_get_px(c, dst, sx, px);
o[tx * 4] = px[0]; o[tx * 4 + 1] = px[1];
o[tx * 4 + 2] = px[2]; o[tx * 4 + 3] = px[3];
}
c->next_ty++;
}
}
/* 交换 prev/cur(prev 直接成为反滤波基准) */
{
unsigned char *t = c->prev;
c->prev = c->cur;
c->cur = t;
}
c->y++;
return 1;
}
#define PNG_NEED_MORE 2
static int png_pump(png_ctx *c)
{
while (c->got < c->need) {
int ret;
c->z.next_out = c->line + c->got;
c->z.avail_out = (unsigned)(c->need - c->got);
ret = inflate(&c->z, Z_NO_FLUSH);
c->got = c->need - c->z.avail_out;
if (ret == Z_STREAM_END) c->z_end = 1;
else if (ret != Z_OK) return 0;
if (c->got < c->need && (c->z.avail_in == 0 || c->z_end))
return c->z_end ? 0 : PNG_NEED_MORE;
}
if (!png_emit_row(c)) return 0;
c->got = 0;
return 1;
}
static int png_bits_of(int color, int depth)
{
switch (color) {
case 0: return depth; /* gray */
case 2: return depth * 3; /* rgb */
case 3: return depth; /* palette */
case 4: return depth * 2; /* gray + a */
case 6: return depth * 4; /* rgba */
default: return 0;
}
}
static int png_depth_ok(int color, int depth)
{
if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16;
if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8;
if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16;
return 0;
}
int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len,
int maxdim, int *tw, int *th, unsigned char **rgba)
{
png_ctx c;
unsigned char sig[8], hdr[8];
unsigned char ibuf[4096];
int have_ihdr = 0, i, ok = 0;
if (maxdim <= 0) return 0;
memset(&c, 0, sizeof c);
c.src.f = f;
c.src.mem = mem;
c.src.memlen = len;
if (!ps_read(&c.src, sig, 8)) return 0;
if (memcmp(sig, "\x89PNG\r\n\x1a\n", 8) != 0) return 0;
while (ps_read(&c.src, hdr, 8)) {
unsigned clen = ((unsigned)hdr[0] << 24) | ((unsigned)hdr[1] << 16) |
((unsigned)hdr[2] << 8) | hdr[3];
const unsigned char *type = hdr + 4;
if (clen > 0x7FFFFFFFu) return 0;
if (memcmp(type, "IHDR", 4) == 0) {
unsigned char d[13];
if (clen < 13 || have_ihdr) return 0;
if (!ps_read(&c.src, d, 13)) return 0;
c.w = ((unsigned)d[0] << 24) | ((unsigned)d[1] << 16) | ((unsigned)d[2] << 8) | d[3];
c.h = ((unsigned)d[4] << 24) | ((unsigned)d[5] << 16) | ((unsigned)d[6] << 8) | d[7];
c.depth = d[8];
c.color = d[9];
if (d[10] != 0 || d[11] != 0 || d[12] != 0) return 0; /* 只支持非交错 + 标准压缩/滤波 */
if (c.w == 0 || c.h == 0 || c.w > 65535u || c.h > 65535u) return 0;
if (!png_depth_ok(c.color, c.depth)) return 0;
c.bits = png_bits_of(c.color, c.depth);
c.rowbytes = ((size_t)c.w * c.bits + 7) / 8;
if (c.rowbytes == 0) return 0;
/* 小图不动:与 meow_img_load_file_dim 的「旧入口」判定一致 */
if ((long)(c.w > c.h ? c.w : c.h) <= maxdim) return 0;
if (clen > 13 && !ps_skip(&c.src, clen - 13)) return 0;
have_ihdr = 1;
c.need = c.rowbytes + 1;
c.line = (unsigned char *)malloc(c.need);
c.prev = (unsigned char *)malloc(c.rowbytes);
c.cur = (unsigned char *)malloc(c.rowbytes);
if (!c.line || !c.prev || !c.cur) goto done;
memset(c.prev, 0, c.rowbytes);
memset(c.cur, 0, c.rowbytes);
for (i = 0; i < 256; i++) c.trns_a[i] = 255;
meow_img_target_dims((int)c.w, (int)c.h, maxdim, &c.tw, &c.th);
c.out = (unsigned char *)malloc((size_t)c.tw * c.th * 4);
if (!c.out) goto done;
memset(c.out, 0, (size_t)c.tw * c.th * 4);
memset(&c.z, 0, sizeof c.z);
if (inflateInit_(&c.z, zlibVersion(), (int)sizeof c.z) != Z_OK) goto done;
c.z_ready = 1;
} else if (memcmp(type, "PLTE", 4) == 0) {
unsigned char d[256 * 3];
if (clen > sizeof d || clen % 3) return 0;
if (!ps_read(&c.src, d, clen)) return 0;
c.pal_n = (int)(clen / 3);
memcpy(c.pal, d, clen);
} else if (memcmp(type, "tRNS", 4) == 0) {
unsigned char d[256];
if (clen > sizeof d) return 0;
if (!ps_read(&c.src, d, clen)) return 0;
c.have_trns = 1;
if (c.color == 3) {
for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i];
} else if (c.color == 0 && clen >= 2) {
/* 8bit/低位深:采样值存在低字节;16bit:与像素一样取高字节比较 */
c.trns[0] = (c.depth == 16) ? d[0] : d[1];
} else if (c.color == 2 && clen >= 6) {
if (c.depth == 16) {
c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4];
} else {
c.trns[0] = d[1]; c.trns[1] = d[3]; c.trns[2] = d[5];
}
}
} else if (memcmp(type, "IDAT", 4) == 0) {
unsigned remain = clen;
if (!have_ihdr || !c.out) return 0;
while (remain) {
unsigned n = remain > sizeof ibuf ? (unsigned)sizeof ibuf : remain;
if (!ps_read(&c.src, ibuf, n)) goto done;
remain -= n;
c.z.next_in = ibuf;
c.z.avail_in = n;
while (c.z.avail_in && c.next_ty < c.th) {
int r = png_pump(&c);
if (r == 0) goto done;
if (r == PNG_NEED_MORE) break;
}
}
} else if (memcmp(type, "IEND", 4) == 0) {
break;
} else {
if (clen && !ps_skip(&c.src, clen)) return 0;
}
/* 跳过 CRC */
{
unsigned char crc[4];
if (!ps_read(&c.src, crc, 4)) break;
}
if (c.out && c.next_ty >= c.th) break; /* 目标行都拿到了,不必读完 */
}
if (!have_ihdr || !c.out || c.next_ty < c.th) goto done;
if (c.color == 3 && c.pal_n <= 0) goto done;
*tw = c.tw;
*th = c.th;
*rgba = c.out;
c.out = NULL;
ok = 1;
done:
if (c.z_ready) inflateEnd(&c.z);
free(c.line);
free(c.prev);
free(c.cur);
free(c.out);
return ok;
}
/* ================================================================== */
/* BMP */
/* ================================================================== */
static unsigned le16(const unsigned char *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); }
static unsigned le32(const unsigned char *p)
{
return (unsigned)p[0] | ((unsigned)p[1] << 8) | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24);
}
int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len,
int maxdim, int *tw, int *th, unsigned char **rgba)
{
png_src src;
unsigned char fh[14], dh[4];
unsigned char dib[1024];
unsigned dibsize, dataoff, bpp, compression, colors;
long w, h, rowsize;
int topdown, i;
unsigned char pal[256][3];
unsigned char *row = NULL, *out = NULL;
int tw_ = 0, th_ = 0, ok = 0;
if (maxdim <= 0) return 0;
memset(&src, 0, sizeof src);
src.f = f;
src.mem = mem;
src.memlen = len;
if (!ps_read(&src, fh, 14)) return 0;
if (fh[0] != 'B' || fh[1] != 'M') return 0;
dataoff = le32(fh + 10);
if (!ps_read(&src, dh, 4)) return 0;
dibsize = le32(dh);
if (dibsize < 40 || dibsize > sizeof dib) return 0; /* BITMAPCOREHEADER/OS2 → stb */
memcpy(dib, dh, 4);
if (!ps_read(&src, dib + 4, dibsize - 4)) return 0;
w = (long)(int)le32(dib + 4);
h = (long)(int)le32(dib + 8);
bpp = le16(dib + 14);
compression = le32(dib + 16);
colors = (dibsize >= 36) ? le32(dib + 32) : 0;
if (compression != 0) return 0; /* RLE/BITFIELDS → stb */
if (w <= 0 || h == 0) return 0;
if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0;
topdown = h < 0;
if (h < 0) h = -h;
if ((long)(w > h ? w : h) <= maxdim) return 0; /* 小图走旧入口 */
if (bpp <= 8) {
unsigned n = colors ? colors : 256;
unsigned char e[4];
if (n > 256) n = 256;
for (i = 0; (unsigned)i < n; i++) {
if (!ps_read(&src, e, 4)) return 0;
pal[i][0] = e[2]; pal[i][1] = e[1]; pal[i][2] = e[0];
}
for (; i < 256; i++) pal[i][0] = pal[i][1] = pal[i][2] = 0;
}
rowsize = (w * bpp + 31) / 32 * 4;
meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_);
row = (unsigned char *)malloc((size_t)rowsize);
out = (unsigned char *)malloc((size_t)tw_ * th_ * 4);
if (!row || !out) goto done;
for (int ty = 0; ty < th_; ty++) {
int sy = (int)((long)ty * h / th_);
long file_row = topdown ? sy : (h - 1 - sy);
long off = (long)dataoff + file_row * rowsize;
unsigned char *o = out + (size_t)ty * tw_ * 4;
if (src.mem) {
if (off < 0 || (size_t)off + (size_t)rowsize > src.memlen) goto done;
memcpy(row, src.mem + off, (size_t)rowsize);
} else {
if (fseek(src.f, off, SEEK_SET) != 0) goto done;
if (fread(row, 1, (size_t)rowsize, src.f) != (size_t)rowsize) goto done;
}
for (int tx = 0; tx < tw_; tx++) {
int sx = (int)((long)tx * w / tw_);
unsigned char *p = o + (size_t)tx * 4;
if (bpp == 24) {
const unsigned char *q = row + sx * 3;
p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255;
} else if (bpp == 32) {
const unsigned char *q = row + sx * 4;
p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; /* BI_RGB:alpha 不作为语义 */
} else if (bpp == 16) {
unsigned v = le16(row + sx * 2);
unsigned r = (v >> 10) & 31, g = (v >> 5) & 31, b = v & 31;
p[0] = (unsigned char)((r << 3) | (r >> 2));
p[1] = (unsigned char)((g << 3) | (g >> 2));
p[2] = (unsigned char)((b << 3) | (b >> 2));
p[3] = 255;
} else {
unsigned idx = row[sx];
p[0] = pal[idx & 255][0]; p[1] = pal[idx & 255][1];
p[2] = pal[idx & 255][2]; p[3] = 255;
}
}
}
*tw = tw_;
*th = th_;
*rgba = out;
out = NULL;
ok = 1;
done:
free(row);
free(out);
return ok;
}
+157
View File
@@ -0,0 +1,157 @@
/*
* meow_select.c — §5.8.6 枚举/预设选择器(M7.2 设置页缺口 #4)。
*
* 封装 = BottomSheet(标题 + 选项列表)+ meow_listview(SINGLE) 单选列表。选项用 meow_listview
* 渲染(单选高亮 primaryContainer、可滚动 meow_scroll 惯性);外层模态复用 meow_ui_modal_*
* + meow_ui_modal_capture/restore(与 meow_dialog 同源:scrim 只叠一层/关闭无残影)。
* 阻塞式(meow_input_pop 轮询);打开前排空残留触摸;点面板外/左缘返回 = 取消;点选项 = 选中 +
* 关闭 + on_change 回调。选项数组定长(≤MEOW_SELECT_MAX_ITEMS)。
*/
#include "meow_ui.h"
#include "include/meow_select.h"
#include "meow_ui_internal.h" /* meow_input_pop/drain + meow_ui_modal_capture/restore */
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#define MEOW_SELECT_ROW 48 /* 选项行高(px) */
struct meow_select {
char title[96];
char items[MEOW_SELECT_MAX_ITEMS][64];
int nitems;
int cur; /* 初始/当前选中项 */
int result; /* 选中下标 / -1=取消 */
int done;
int h; /* 面板高度(0=自动) */
meow_select_fn cb;
void *ctx;
};
/* meow_listview src2 数据源:填 title=options[idx](右尾件无;enabled=1)。 */
static int select_src2(int idx, meow_list_item *out, void *ctx)
{
meow_select *s = (meow_select *)ctx;
if (idx < 0 || idx >= s->nitems) return 0;
memset(out, 0, sizeof *out);
out->title = s->items[idx];
out->right = MEOW_LIST_RIGHT_NONE;
out->enabled = 1;
return 1;
}
meow_select *meow_select_create(const char *title, int cur)
{
meow_select *s = (meow_select *)calloc(1, sizeof *s);
if (!s) return NULL;
if (title) { strncpy(s->title, title, sizeof s->title - 1); s->title[sizeof s->title - 1] = '\0'; }
s->cur = cur; s->result = -1; s->done = 0;
return s;
}
int meow_select_add_item(meow_select *s, const char *label)
{
if (!s || s->nitems >= MEOW_SELECT_MAX_ITEMS) return -1;
if (label) { strncpy(s->items[s->nitems], label, 63); s->items[s->nitems][63] = '\0'; }
int idx = s->nitems;
s->nitems++;
return idx;
}
void meow_select_set_on_change(meow_select *s, meow_select_fn cb, void *ctx)
{ if (s) { s->cb = cb; s->ctx = ctx; } }
int meow_select_open(meow_select *s)
{
if (!s || s->nitems <= 0) return -1;
const meow_theme *th = meow_theme_get();
meow_input_start();
meow_input_drain(); /* 排空残留触摸(goodix 长尾) */
s->done = 0; s->result = -1;
int W = meow_ui_width(), H = meow_ui_height();
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
int pad = 24;
int title_h = meow_ui_text_height(th->font_title);
int rowh = MEOW_SELECT_ROW;
int avail = H - sb - 20;
int min_h = pad + title_h + 12 + rowh + 20;
int want = (s->h > 0) ? s->h : min_h;
int est = pad + title_h + 12 + s->nitems * rowh + 20;
if (est > want) want = est;
if (want > avail) want = avail;
if (want < min_h) want = (min_h < avail) ? min_h : avail;
int ph = want, py = H - ph; /* 底部面板(顶部圆角) */
int list_y = py + pad + title_h + 12;
int list_h = ph - (list_y - py) - pad; /* 底部余量 */
if (list_h < rowh) list_h = rowh;
/* 选项列表(meow_listview[SINGLE]:单选高亮 + 可滚动)。open 时创建,close 释放(冷路径)。 */
meow_widget *lv = meow_listview_create(0, list_y, W, list_h, s->nitems,
select_src2, s, MEOW_LIST_SEL_SINGLE);
if (!lv) return -1;
meow_listview_set_item_h(lv, rowh);
/* 定位初始选中项在视口内。 */
int target = s->cur * rowh - (list_h - rowh) / 2;
int maxs = meow_listview_max_scroll(lv);
if (target < 0) target = 0;
if (target > maxs) target = maxs;
meow_listview_set_scroll(lv, target);
meow_listview_set_sel(lv, s->cur);
meow_scroll *sc = meow_scroll_wrap(lv, 1); /* 惯性滚动(fling/snap) */
/* §6.4 模态作用域 + 背景快照(关闭即还原,无残影/不叠黑)。 */
meow_ui_modal *m = meow_ui_modal_begin();
meow_ui_modal_capture();
/* 首次绘制:遮罩 + 底部面板(顶部圆角)+ 标题 + 选项列表。 */
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest);
meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface);
meow_widget_draw(lv);
meow_ui_flush();
int dy_start = 0, down = 0;
while (!s->done) {
meow_input_ev e;
while (meow_input_pop(&e)) {
meow_ui_activity_ping(); /* 模态期间交互也重置息屏计时 */
if (e.type == MEOW_EV_TOUCH_DOWN) { down = 1; dy_start = e.b; }
else if (e.type == MEOW_EV_TOUCH_MOVE) {
if (down && sc) {
meow_scroll_track(sc, e.b - dy_start, meow_ui_time_ms());
dy_start = e.b;
}
} else if (e.type == MEOW_EV_TAP) {
int x = e.a, y = e.b;
if (y < py) { s->done = 1; s->result = -1; break; } /* 点面板外 = 取消 */
int idx = meow_listview_tap_at(lv, x, y); /* 单选高亮 = 选中 */
if (idx >= 0) {
s->result = idx; s->cur = idx;
if (s->cb) s->cb(idx, s->ctx); /* 关闭即回调 */
s->done = 1;
}
/* 面板内但未命中项:保持打开(等下一次点击)。 */
} else if (e.type == MEOW_EV_SWIPE) {
if (e.a == MEOW_SWIPE_LEFT) { s->done = 1; s->result = -1; } /* 返回 = 取消 */
else if (sc) meow_scroll_fling(sc);
}
}
meow_anim_tick(); /* 推进 fling */
if (!s->done) usleep(16000);
}
meow_ui_modal_end(m);
meow_ui_modal_restore();
if (sc) meow_scroll_free(sc);
meow_widget_free(lv);
return s->result;
}
void meow_select_free(meow_select *s) { if (s) free(s); }
int meow_select_show(const char *title, const char *const *opts, int n, int cur)
{
if (!opts || n <= 0) return -1;
meow_select *s = meow_select_create(title, cur);
if (!s) return -1;
for (int i = 0; i < n && i < MEOW_SELECT_MAX_ITEMS; i++) meow_select_add_item(s, opts[i]);
int r = meow_select_open(s);
meow_select_free(s);
return r;
}
+15 -1
View File
@@ -32,8 +32,14 @@
* include/meow_ui_log.h 应用日志(§9)
* include/meow_ui_notify.h 应用通知(§9.1)
* include/meow_ui_config.h 应用配置读写(§9.2)
* include/meow_ui_font.h 字体列表(§5.7)+ 字体分层(§10)
* include/meow_ui_font.h 字体列表(§5.7)+ 字体分层(§10)+ 矢量字 meow_ft_*(§10.1)
* include/meow_ui_audio.h 独占音频客户端(§11)
* include/meow_ui_box.h 线性布局引擎(§12.1:meow_box,固定/weight/wrap/对齐/嵌套)
* include/meow_ui_anim.h 动画基座+UI 节拍器(§12.2:tween/easing/池+脏区联动)
* include/meow_ui_scroll.h 滚动物理(§12.3:速度跟踪/fling/边界回弹/snap)
* include/meow_lazygrid.h 2 维虚拟化宫格(§12.4:meow_lazygrid,M7.4 主界面前置)
* include/meow_select.h 枚举/预设选择器(M7.2 设置页缺口 #4:BottomSheet+单选列表)
* include/meow_ui_str.h 字符串资源/i18n(§13:meow_str/meow_strf/meow_str_set_locale)
*
* 其余模块头(非伞头、App 按需显式 include,均保持不动):
* meow_ui_view.h / meow_ui_nav.h / meow_ui_media.h / meow_ui_sys.h / meow_ui_vol.h
@@ -63,5 +69,13 @@
#include "include/meow_ui_config.h"
#include "include/meow_ui_font.h"
#include "include/meow_ui_audio.h"
#include "include/meow_ui_box.h"
#include "include/meow_ui_anim.h"
#include "include/meow_ui_scroll.h"
#include "include/meow_lazygrid.h"
#include "include/meow_select.h"
#include "include/meow_ui_str.h"
#include "include/meow_ui_net.h" /* 期4 网络自愿通道:meow_net_policy_check / meow_net_report */
#include "include/meow_cachegov.h" /* 磁盘缓存 page-cache 治理(A:DONTNEED L0/L1 + refault 回退;C 归 svc_perf) */
#endif /* MEOW_UI_H */
+128
View File
@@ -0,0 +1,128 @@
/*
* meow_ui_anim.c — 动画基座 + UI 节拍器(§12.2)。
*
* 时间驱动 tween:start 记录起始时刻(meow_ui_time_ms,单调),tick 按经过时间求 t,
* 代入 ease 得 v = from + (to-from)*ease(t),回调 update;t 到 1 → 回调 done(canceled=0) 并回收。
* 取消 → 回调 done(canceled=1) 并回收。静态池(≤MEOW_ANIM_MAX),热路径零 malloc。
* 无动画时 meow_anim_tick() 仅查活动计数即返回(零开销)。
*
* 节拍挂接:由 meow_ui_run 主循环(on_tick 之后)与 meow_ui_feed_tick(阻塞 IME 循环)调用,
* 不引入常驻高频轮询。modal/dialog 阻塞循环不喂 tick(动画暂停,符合「模态期不做动画」惯例)。
*/
#include "meow_ui.h"
#include "include/meow_ui_anim.h"
#include <string.h>
struct meow_anim {
int active;
float from, to;
uint32_t duration_ms;
meow_ease ease;
uint64_t start_ms;
float value;
int region_set;
int rx, ry, rw, rh;
meow_anim_update_fn update;
meow_anim_done_fn done;
void *ctx;
};
static struct meow_anim g_pool[MEOW_ANIM_MAX];
static unsigned g_active = 0; /* 活动动画计数(0 = 空闲,tick 直退) */
/* ---- 缓动 ---- */
float meow_ease_apply(meow_ease e, float t)
{
if (e == MEOW_EASE_OUT) {
/* ease-out quad */
float u = 1.0f - t;
return 1.0f - u * u;
}
if (e == MEOW_EASE_IN_OUT) {
/* ease-in-out quad */
if (t < 0.5f) return 2.0f * t * t;
float u = -2.0f * t + 2.0f;
return 1.0f - u * u * 0.5f;
}
if (e == MEOW_EASE_OVERSHOOT) {
/* ease-out-back(越过目标再回稳);t=1 → 恰为 1 */
const float c1 = 1.70158f;
const float c3 = c1 + 1.0f;
float u = t - 1.0f;
return 1.0f + c3 * u * u * u + c1 * u * u;
}
/* MEOW_EASE_LINEAR */
return t;
}
/* ---- 生命周期 ---- */
meow_anim *meow_anim_start(const meow_anim_desc *d)
{
if (!d) return NULL;
for (int i = 0; i < MEOW_ANIM_MAX; i++) {
struct meow_anim *a = &g_pool[i];
if (a->active) continue;
a->active = 1;
a->from = d->from;
a->to = d->to;
a->duration_ms = d->duration_ms ? d->duration_ms : 1; /* 0 → 瞬发(下一 tick 完成) */
a->ease = d->ease;
a->start_ms = meow_ui_time_ms();
a->update = d->update;
a->done = d->done;
a->ctx = d->ctx;
a->region_set = 0;
a->value = d->from;
g_active++;
if (a->update) a->update(a, a->value, a->ctx); /* 起始帧回调(t=0) */
return a;
}
return NULL; /* 池满 */
}
void meow_anim_cancel(meow_anim *a)
{
if (!a || !a->active) return;
a->active = 0;
if (g_active > 0) g_active--;
if (a->done) a->done(a, 1, a->ctx);
}
int meow_anim_active(meow_anim *a) { return (a && a->active) ? 1 : 0; }
float meow_anim_value(meow_anim *a) { return a ? a->value : 0.0f; }
void meow_anim_set_region(meow_anim *a, int x, int y, int w, int h)
{
if (!a) return;
if (x < 0) { a->region_set = 0; return; }
a->region_set = 1;
a->rx = x; a->ry = y; a->rw = w > 0 ? w : 0; a->rh = h > 0 ? h : 0;
}
/* ---- 节拍器 ---- */
unsigned meow_anim_count(void) { return (unsigned)g_active; }
void meow_anim_tick(void)
{
if (g_active == 0) return; /* 无动画:零开销直接返回 */
uint64_t now = meow_ui_time_ms();
for (int i = 0; i < MEOW_ANIM_MAX; i++) {
struct meow_anim *a = &g_pool[i];
if (!a->active) continue;
uint64_t elapsed = now - a->start_ms;
float t = (float)(elapsed) / (float)a->duration_ms;
int finished = (t >= 1.0f);
if (t > 1.0f) t = 1.0f;
if (t < 0.0f) t = 0.0f;
float v = a->from + (a->to - a->from) * meow_ease_apply(a->ease, t);
a->value = v;
if (a->update) a->update(a, v, a->ctx);
if (a->region_set && a->rw > 0 && a->rh > 0)
meow_ui_flush_region(a->rx, a->ry, a->rw, a->rh); /* 只刷动画影响区 */
if (finished) {
a->active = 0;
if (g_active > 0) g_active--;
if (a->done) a->done(a, 0, a->ctx);
}
}
}
+40 -5
View File
@@ -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);
+307
View File
@@ -0,0 +1,307 @@
/*
* meow_ui_box.c — 线性布局引擎(§12.1 meow_box)。
*
* meow_box 是**纯布局计算器**(非控件):静态池创建、定长子项数组、arrange 单遍求解,
* 把每个子控件的 x/y/w/h 写进 meow_widget(meow_widget_set_rect);嵌套 sub-box 递归求解。
* 用显式静态栈(深度≤MEOW_BOX_MAX_DEPTH)做嵌套展开,无 C 递归,杜绝栈爆炸。
*
* 求解模型(主轴向):
* - 每个子项占位 = lead(margin) + extent + trail(margin);项间再叠 box->gap。
* - FIXED/WRAP 的 extent 先定;WEIGHT 的 extent=0(待均分)。
* - leftover = content_main - foot;若权重和>0 → 均分 leftover;否则按主轴对齐分配。
* 交叉轴:按 START/CENTER/END/STRETCH 对齐,STRETCH 填满容器交叉向减 margin。
*/
#include "meow_ui.h"
#include "include/meow_ui_box.h"
#include "meow_ui_internal.h" /* meow_widget_rect(只读几何) */
#include <string.h>
#define MEOW_BOX_POOL 16 /* 静态池大小(并发 box 数) */
struct meow_box {
int dir; /* MEOW_BOX_H / MEOW_BOX_V */
int x, y, w, h; /* 容器矩形(绝对坐标) */
int pad_l, pad_t, pad_r, pad_b;
int gap;
int main_align;
int cross_align;
meow_box_item items[MEOW_BOX_MAX_CHILD];
int n;
};
static meow_box g_pool[MEOW_BOX_POOL];
static unsigned char g_used[MEOW_BOX_POOL];
/* ---- 静态池存取 ---- */
meow_box *meow_box_new(meow_box_dir dir)
{
for (int i = 0; i < MEOW_BOX_POOL; i++) {
if (!g_used[i]) {
g_used[i] = 1;
meow_box *b = &g_pool[i];
memset(b, 0, sizeof *b);
b->dir = (dir == MEOW_BOX_H) ? MEOW_BOX_H : MEOW_BOX_V;
return b;
}
}
return NULL;
}
void meow_box_free(meow_box *b)
{
if (!b) return;
for (int i = 0; i < MEOW_BOX_POOL; i++)
if (&g_pool[i] == b) { g_used[i] = 0; return; }
}
void meow_box_set_rect(meow_box *b, int x, int y, int w, int h)
{
if (!b) return;
b->x = x; b->y = y;
b->w = w > 0 ? w : 0; b->h = h > 0 ? h : 0;
}
void meow_box_set_padding(meow_box *b, int l, int t, int r, int btm)
{
if (!b) return;
b->pad_l = l; b->pad_t = t; b->pad_r = r; b->pad_b = btm;
}
void meow_box_set_gap(meow_box *b, int gap)
{
if (!b) return;
b->gap = gap > 0 ? gap : 0;
}
void meow_box_set_align(meow_box *b, int main_align, int cross_align)
{
if (!b) return;
b->main_align = main_align;
b->cross_align = cross_align;
}
/* ---- 子项 ---- */
int meow_box_add(meow_box *b, meow_widget *w, int size, int weight)
{
return meow_box_add_ex(b, w, size, weight, 0, 0, NULL, NULL);
}
int meow_box_add_ex(meow_box *b, meow_widget *w, int size, int weight,
int min, int preferred, meow_box_measure_fn m, void *mctx)
{
if (!b || !w || b->n >= MEOW_BOX_MAX_CHILD) return -1;
if (size != MEOW_BOX_FIXED && size != MEOW_BOX_WEIGHT && size != MEOW_BOX_WRAP) return -1;
if (size == MEOW_BOX_WEIGHT && weight <= 0) return -1;
for (int i = 0; i < b->n; i++)
if (b->items[i].w == w) return -1; /* 同控件去重 */
meow_box_item *it = &b->items[b->n++];
memset(it, 0, sizeof *it);
it->w = w; it->size = size; it->weight = weight > 0 ? weight : 1;
it->min = min > 0 ? min : 0;
it->preferred = preferred > 0 ? preferred : 0;
it->measure = m; it->mctx = mctx;
return 0;
}
int meow_box_add_sub(meow_box *b, meow_box *sub, int size, int weight)
{
if (!b || !sub || b->n >= MEOW_BOX_MAX_CHILD) return -1;
if (size != MEOW_BOX_FIXED && size != MEOW_BOX_WEIGHT && size != MEOW_BOX_WRAP) return -1;
if (size == MEOW_BOX_WEIGHT && weight <= 0) return -1;
for (int i = 0; i < b->n; i++)
if (b->items[i].sub == sub) return -1;
meow_box_item *it = &b->items[b->n++];
memset(it, 0, sizeof *it);
it->sub = sub; it->size = size; it->weight = weight > 0 ? weight : 1;
return 0;
}
/* ---- 查询 ---- */
int meow_box_count(const meow_box *b) { return b ? b->n : 0; }
meow_widget *meow_box_widget_at(const meow_box *b, int idx)
{ return (b && idx >= 0 && idx < b->n) ? b->items[idx].w : NULL; }
meow_box *meow_box_sub_at(const meow_box *b, int idx)
{ return (b && idx >= 0 && idx < b->n) ? b->items[idx].sub : NULL; }
void meow_box_rect(const meow_box *b, int out[4])
{
if (!out) return;
if (!b) { out[0] = out[1] = out[2] = out[3] = 0; return; }
out[0] = b->x; out[1] = b->y; out[2] = b->w; out[3] = b->h;
}
/* ---- 内部:取子项 margin(widget opts;sub 无 margin)---- */
static void item_margin(const meow_box_item *it, meow_box_dir dir,
int *main_before, int *main_after, int *cross)
{
*main_before = *main_after = *cross = 0;
if (!it->w) return;
meow_widget_opts o;
if (meow_widget_opts_get(it->w, &o) != 0) return;
int mx = o.margin_x > 0 ? o.margin_x : 0;
int my = o.margin_y > 0 ? o.margin_y : 0;
if (dir == MEOW_BOX_H) { *main_before = mx; *main_after = mx; *cross = my; }
else { *main_before = my; *main_after = my; *cross = mx; }
}
/* ---- 内部:子项主轴向 extent ----
* FIXED → 控件/嵌套盒当前主轴向尺寸
* WRAP → measure 回调(若有)否则控件当前尺寸;下限 clamp 到 min
* WEIGHT → 0(由均分阶段填充) */
static int item_extent(const meow_box_item *it, int main_is_w)
{
int extent = 0;
if (it->w) {
int r[4]; meow_widget_rect(it->w, r);
extent = main_is_w ? r[2] : r[3];
} else if (it->sub) {
extent = main_is_w ? it->sub->w : it->sub->h;
}
if (it->size == MEOW_BOX_WRAP && it->measure) {
int mn = 0, pf = 0;
it->measure(it->mctx, &mn, &pf);
if (pf > 0) extent = pf;
else if (mn > 0) extent = mn;
if (mn > 0 && extent < mn) extent = mn; /* clamp 到下限 */
}
if (extent < 0) extent = 0;
return extent;
}
/* ---- 单容器求解(一次 pass:算 extent + 位置,写全部子项矩形)----
* 子项为嵌套 meow_box 时只写其矩形(父求结果),后续由 meow_box_arrange 的
* 显式栈递归求解其内部。 */
static void box_layout_one(meow_box *b)
{
int n = b->n;
if (n <= 0) return;
int cx = b->x + b->pad_l;
int cy = b->y + b->pad_t;
int cw = b->w - b->pad_l - b->pad_r; if (cw < 0) cw = 0;
int ch = b->h - b->pad_t - b->pad_b; if (ch < 0) ch = 0;
int main_is_w = (b->dir == MEOW_BOX_H);
int main_size = main_is_w ? cw : ch;
int cross_size = main_is_w ? ch : cw;
int gap = b->gap;
int ext[MEOW_BOX_MAX_CHILD], lead[MEOW_BOX_MAX_CHILD],
trail[MEOW_BOX_MAX_CHILD], wgt[MEOW_BOX_MAX_CHILD];
int nw = 0, wsum = 0;
long long foot = 0;
for (int i = 0; i < n; i++) {
const meow_box_item *it = &b->items[i];
int mb, ma, cr;
item_margin(it, (meow_box_dir)b->dir, &mb, &ma, &cr);
lead[i] = mb; trail[i] = ma; (void)cr;
wgt[i] = (it->size == MEOW_BOX_WEIGHT) ? it->weight : 0;
if (it->size == MEOW_BOX_WEIGHT) {
ext[i] = 0; nw++; wsum += it->weight;
} else {
ext[i] = item_extent(it, main_is_w);
}
foot += (long long)lead[i] + trail[i] + ext[i];
}
foot += (long long)gap * (n - 1);
long long leftover = (long long)main_size - foot;
if (nw > 0 && leftover > 0) {
/* 均分剩余到权重项;余数塞给最后一个权重项铺满精确 */
long long consumed = 0;
for (int i = 0; i < n; i++) {
if (wgt[i] > 0) { ext[i] = (int)(leftover * wgt[i] / wsum); consumed += ext[i]; }
}
for (int i = n - 1; i >= 0; i--) {
if (wgt[i] > 0) { ext[i] += (int)(leftover - consumed); break; }
}
leftover = 0;
}
/* 主轴对齐(仅当无权重、leftover != 0 有意义;有权重时 leftover≈0 → START 等效) */
int off0 = 0;
int intergap = gap;
if (nw == 0) {
if (leftover > 0) {
switch (b->main_align) {
case MEOW_BOX_ALIGN_CENTER: off0 = (int)(leftover / 2); break;
case MEOW_BOX_ALIGN_END: off0 = (int)leftover; break;
case MEOW_BOX_ALIGN_SPACE_BETWEEN: off0 = 0;
intergap = (n > 1) ? gap + (int)(leftover / (n - 1)) : gap; break;
case MEOW_BOX_ALIGN_SPACE_AROUND: off0 = (int)(leftover / (2 * n));
intergap = gap + (int)(leftover / n); break;
case MEOW_BOX_ALIGN_SPACE_EVENLY: off0 = (int)(leftover / (n + 1));
intergap = gap + (int)(leftover / (n + 1)); break;
default: off0 = 0; break; /* START */
}
} else {
off0 = 0; intergap = gap; /* 溢出:从起点排(可越界,交滚动容器) */
}
}
/* 主轴定位 + 交叉轴定位,写子控件矩形(主轴起点 = 内容原点 cx/cy)。
* rel = 相对内容原点的主轴游标;content_rel = 本项内容相对起点;content_pos = 绝对主轴坐标。 */
int main_base = main_is_w ? cx : cy;
int rel = off0;
for (int i = 0; i < n; i++) {
const meow_box_item *it = &b->items[i];
int content_rel = rel + lead[i];
int content_pos = main_base + content_rel;
if (it->w) {
int r[4]; meow_widget_rect(it->w, r);
meow_widget_opts o; int has = (meow_widget_opts_get(it->w, &o) == 0);
int cmx = (has && o.margin_x > 0) ? o.margin_x : 0;
int cmy = (has && o.margin_y > 0) ? o.margin_y : 0;
int cm = main_is_w ? cmy : cmx;
int base = main_is_w ? r[3] : r[2];
int cross = 0, cross_ext = 0;
if (b->cross_align == MEOW_BOX_CROSS_STRETCH) {
cross_ext = cross_size - 2 * cm; if (cross_ext < 0) cross_ext = 0;
cross = cm;
} else {
cross_ext = base;
int pad = cross_size - 2 * cm - cross_ext; if (pad < 0) pad = 0;
if (b->cross_align == MEOW_BOX_CROSS_CENTER) cross = cm + pad / 2;
else if (b->cross_align == MEOW_BOX_CROSS_END) cross = cm + pad;
else cross = cm; /* START */
}
if (main_is_w)
meow_widget_set_rect(it->w, content_pos, cy + cross, ext[i], cross_ext);
else
meow_widget_set_rect(it->w, cx + cross, content_pos, cross_ext, ext[i]);
} else if (it->sub) {
meow_box *sb = it->sub;
int base = main_is_w ? sb->h : sb->w;
int cross = 0, cross_ext = 0;
if (b->cross_align == MEOW_BOX_CROSS_STRETCH) {
cross_ext = cross_size; if (cross_ext < 0) cross_ext = 0;
cross = 0;
} else {
cross_ext = base;
int pad = cross_size - cross_ext; if (pad < 0) pad = 0;
if (b->cross_align == MEOW_BOX_CROSS_CENTER) cross = pad / 2;
else if (b->cross_align == MEOW_BOX_CROSS_END) cross = pad;
else cross = 0;
}
if (main_is_w) { sb->x = content_pos; sb->y = cy + cross; sb->w = ext[i]; sb->h = cross_ext; }
else { sb->x = cx + cross; sb->y = content_pos; sb->w = cross_ext; sb->h = ext[i]; }
}
if (i < n - 1)
rel = content_rel + ext[i] + trail[i] + intergap;
else
rel = content_rel + ext[i] + trail[i];
}
}
/* ---- 求解:单遍,显式静态栈展开嵌套(深度≤MEOW_BOX_MAX_DEPTH)---- */
static meow_box *g_stack[MEOW_BOX_MAX_DEPTH];
void meow_box_arrange(meow_box *b)
{
if (!b) return;
int top = 0;
g_stack[0] = b;
while (top >= 0) {
meow_box *cur = g_stack[top--];
box_layout_one(cur); /* 写全部直接子项矩形(含 sub->rect) */
for (int i = 0; i < cur->n; i++) {
meow_box *sb = cur->items[i].sub;
if (!sb) continue;
if (top + 1 >= MEOW_BOX_MAX_DEPTH) continue; /* 深度上限:跳过(矩形已被父写定) */
g_stack[++top] = sb;
}
}
}
+19 -12
View File
@@ -27,7 +27,7 @@
static unsigned char *g_modal_cap = NULL;
static int g_modal_cap_line = 0, g_modal_cap_h = 0, g_modal_cap_bpp = 0;
static void modal_capture(void)
void meow_ui_modal_capture(void)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch();
@@ -43,7 +43,7 @@ static void modal_capture(void)
if (g_modal_cap) memcpy(g_modal_cap, bb, n);
}
static void modal_restore(void)
void meow_ui_modal_restore(void)
{
unsigned char *bb = meow_ui_back_buf();
if (!g_modal_cap || !bb) return;
@@ -100,8 +100,8 @@ static void dlg_calc(meow_dialog *d, const meow_theme *th, dlg_geom *g)
static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g)
{
int pad = 24;
meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->surfaceContainerHighest);
meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->outline);
meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest);
meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->outline);
meow_ui_text(g->dx + pad, g->dy + pad, d->title, th->font_title, th->onSurface);
if (d->type == MEOW_DLG_SELECT) {
@@ -125,7 +125,7 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g)
int y = g->row_y;
if (d->type == MEOW_DLG_INPUT) {
char buf[300];
snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : "(tap OK to edit...)");
snprintf(buf, sizeof buf, "%s", d->ime.buf[0] ? d->ime.buf : meow_str("common.tap_ok_edit"));
meow_ui_text(g->dx + pad, y, buf, th->font_body, th->onSurfaceVariant);
} else {
for (int i = 0; i < nmsg; i++) {
@@ -138,10 +138,10 @@ static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g)
int bx = g->dx + pad, cx = bx + g->btn_w + 12, by = g->btn_y;
meow_ui_draw_rect_round(bx, by, g->btn_w, g->btn_h, 10, th->primary);
meow_ui_text_center(bx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2,
"CANCEL", th->font_body, th->primary);
meow_str("common.cancel"), th->font_body, th->primary);
meow_gfx_fill_round(cx, by, g->btn_w, g->btn_h, 10, th->primary);
meow_ui_text_center(cx + g->btn_w / 2, by + (g->btn_h - meow_ui_text_height(th->font_body)) / 2,
"OK", th->font_body, th->onPrimary);
meow_str("common.ok"), th->font_body, th->onPrimary);
}
}
@@ -232,7 +232,11 @@ void meow_dialog_show(meow_dialog *d)
meow_input_start();
meow_input_drain(); /* 排空残留触摸(goodix 长尾) */
d->done = 0; d->result = 0;
modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */
/* §6.4:模态作用域——本层打开期间下层(view/widget)完全收不到触摸;点卡片外由背板消费
* (d->done=1 result=0 取消),不透传。嵌套弹层 depth 递增。 */
meow_ui_modal *m = meow_ui_modal_begin();
meow_ui_modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影);
* 嵌套模态在各自打开时拍「当前已渲染帧」,故两层弹开不叠黑。 */
dlg_paint(d, th);
meow_ui_flush();
@@ -286,7 +290,8 @@ void meow_dialog_show(meow_dialog *d)
if (d->done) break;
usleep(16000);
}
modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
meow_ui_modal_end(m); /* §6.4:关闭模态作用域(下层恢复接收触摸) */
meow_ui_modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
}
int meow_dialog_result(meow_dialog *d) { return d ? d->result : 0; }
@@ -349,10 +354,11 @@ int meow_sheet_open(meow_sheet *s)
int W = meow_ui_width(), H = meow_ui_height();
int ph = sheet_h(s, th);
int py = H - ph, pad = 24;
modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原 */
meow_ui_modal *m = meow_ui_modal_begin(); /* §6.4:模态作用域(下层收不到触摸) */
meow_ui_modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */
/* 首次绘制:遮罩 + 底部面板(顶部圆角) */
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest);
meow_gfx_fill_round(0, py, W, ph, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest);
meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface);
for (int i = 0; i < s->nitems; i++) {
int ry = py + pad + meow_ui_text_height(th->font_title) + 12 + i * MEOW_MODAL_ROW;
@@ -382,7 +388,8 @@ int meow_sheet_open(meow_sheet *s)
if (s->done) break;
usleep(16000);
}
modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
meow_ui_modal_end(m); /* §6.4:关闭模态作用域 */
meow_ui_modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
return s->result;
}
void meow_sheet_free(meow_sheet *s) { if (s) free(s); }
+22
View File
@@ -266,6 +266,28 @@ void meow_draw_view_set_z(struct meow_view *v, int z)
}
}
/* ============================================================ */
/* §6.4 自上而下命中(事件分发形式化) */
/* ============================================================ */
void *meow_draw_hit_topdown(int x, int y)
{
void *best = NULL;
int bestz = -1, bestseq = -1;
for (int i = 0; i < g_n; i++) {
draw_entry *e = &g_e[i];
e_compute_auto(e);
/* 只对注册控件做顶层命中;view 是容器(其子 widget 是独立更高 z 的绘制物,各自命中),
* CUSTOM 是 App 直绘(不参与命中)。meow_widget_hit 内部含 owner==active + enable +
* 矩形/自定义命中回调。 */
if (e->kind != MEOW_ITEM_WIDGET || !e->w) continue;
if (!meow_widget_hit(e->w, x, y)) continue;
if (e->z > bestz || (e->z == bestz && e->seq > bestseq)) {
bestz = e->z; bestseq = e->seq; best = e->w;
}
}
return best;
}
/* ============================================================ */
/* page bg / 区域重画 / 整帧 */
/* ============================================================ */
+216 -39
View File
@@ -12,7 +12,7 @@
#include "meow_ui_internal.h"
#include "meow_ui_sys.h"
#include "sb_icons.h"
#include "meow_font.h" /* 公开 SDK 仓内 vendored(include/meow_font.h),不再引主仓 launcher 头 */
#include "meow_font.h"
#include <stdio.h>
#include <stdlib.h>
@@ -20,7 +20,6 @@
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
@@ -172,15 +171,9 @@ static void refresh_governor(unsigned long long cost_ms, int period)
}
/* ---- 工具 ---- */
static uint64_t now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
uint64_t meow_ui_time_ms(void) { return now_ms(); }
/* 时钟统一由 meow_ui_time.c 提供(meow_ui_time_ms,声明见 include/meow_ui_draw.h):
* mipsel 上 zig musl 的 time64 ABI 自相矛盾,取时必须走内核 o32 ABI 再升位,
* 故本 TU 不再保留取时实现(详见 sdk/CLOCK_ABI.md)。 */
static uint16_t rgb565(uint32_t c)
{
return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F));
@@ -254,22 +247,21 @@ int meow_ui_back_buf_bpp(void) { return g_bpp; }
* 从而 launcher 与 app 统一渲染栈(同一离屏 + 同一 present)。 */
unsigned char *meow_ui_back_buf(void) { return g_back_buf; }
/* ================= 状态栏 MD3 图标 blit(写进本 SDK back_buf) =================
* 复用 launcher sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)作为状态栏
* 右侧图标簇(「同一个状态栏」图标式,见 meow_ui_sys_statusbar_render)。
* idx = sb_icons[] 索引(SB_I_*,见 meow_ui_internal.h)
/* ================= MD3 图标 blit(写进本 SDK back_buf) =================
* 复用 sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)——状态栏右侧图标簇
* (meow_ui_sys_statusbar_render)与 TopAppBar 位图动作图标(W_TOPAPPBAR)共用同源位图。
* idx = sb_icons[] 索引(状态栏 SB_I_*、动作 MEOW_ICON_*,见 meow_ui_internal.h)
* x = 图标槽左缘(槽宽 SB_ICON_SIZE=32)
* y = 图标槽顶(仅作参考;实际按 ink 垂直中心对齐 cy)
* ink_h = 目标 ink 高(>0 等比缩放;0 = 保持位图自然 ink 尺寸——WiFi 强弱档用)
* fg = 24-bit RGB 前景(主题 on_surface / on_surface_variant,日/夜由主题表提供)
* cy = ink 垂直中心线(= 状态栏文字中心,28px 栏约 14)
* cy = ink 垂直中心线(状态栏=28px 栏约 14;TopAppBar=栏中点)
* 先求位图 ink 边界(阈值 32,忽略近透明抗锯齿),按 ink 在槽内水平居中、垂直中心对齐 cy;
* 像素按 alpha 软边混合进 back_buf(自 g_fb/g_line/g_xres/g_yres,与 launcher sb_blit_icon 同源算法)。 */
void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy)
/* 像素核心:只采样混合进 back_buf,不标脏区。供状态栏路径(blit 后另标顶栏脏区)与
* 控件路径(meow_gfx_blit_icon_ink,控件 invalidation 独立管理脏区)共用,避免状态栏的
* 顶栏脏区假设污染控件绘制(topbar 不在屏幕最顶行时标错区域)。 */
static void icon_blit_px(int idx, int x, int ink_h, uint32_t fg, int cy)
{
(void)y;
if (!g_init_ok || !g_fb) return;
if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return;
const unsigned char *src = sb_icons[idx].data;
uint16_t col = rgb565(fg);
/* 1) ink 边界(含空图兜底) */
@@ -277,8 +269,10 @@ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy)
for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) {
if (src[i] >= 32) {
int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE;
if (xx < minx) minx = xx; if (xx > maxx) maxx = xx;
if (yy < miny) miny = yy; if (yy > maxy) maxy = yy;
if (xx < minx) minx = xx;
if (xx > maxx) maxx = xx;
if (yy < miny) miny = yy;
if (yy > maxy) maxy = yy;
}
}
if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; }
@@ -311,10 +305,28 @@ void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy)
row[tx] = (uint16_t)((r << 11) | (g << 5) | b);
}
}
/* 整条顶栏作为脏区标出(图标在顶条内,present 只重画顶条即可) */
}
/* 状态栏路径:blit 像素 + 把整条顶栏标脏(present 只重画顶条即可)。 */
void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy)
{
(void)y;
if (!g_init_ok || !g_fb) return;
if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return;
icon_blit_px(idx, x, ink_h, fg, cy);
dirty_add(x, 0, SB_ICON_SIZE, MUI_SBAR_H);
}
/* 控件路径(TopAppBar 动作图标):只写像素,不标「顶栏」脏区——控件矩形由
* meow_widget_draw 的 clip + invalidation(setter 标脏)独立管理,避免把状态栏的
* 顶栏脏区假设带进控件绘制(topbar 不位于屏幕最顶行时标错区域、触发误重画)。 */
void meow_gfx_blit_icon_ink(int idx, int x, int ink_h, uint32_t fg, int cy)
{
if (!g_init_ok || !g_fb) return;
if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return;
icon_blit_px(idx, x, ink_h, fg, cy);
}
/* ================= 像素写入 ================= */
static void px_write_rgb565(int x, int y, uint16_t c16)
{
@@ -811,6 +823,17 @@ int meow_ui_text_height(int size)
return 7 * fallback_scale(size);
}
/* ================= 矢量字(FreeType)渲染薄封装(§10.1) =================
* meow_ft_text 在 meow_ui_ft.c(独立 .o,App 显式调 meow_ft_* 才拉取整条 FreeType 依赖),
* 它经本处两个薄封装写进 back_buf:blend 一个灰度像素(过 clip)、标一块脏区。
* 这两函数在 gfx.o 里(任何 App 本就拉取 gfx.o),ft.o 引用它们不引入新的未拉入成员,
* 从而保证「不调 meow_ft_* 的 App 零增量」(freetype .o 不会被拉入)。 */
void meow_gfx_ft_blend_px(int x, int y, uint32_t color, int alpha)
{ put_px_blend(x, y, color, alpha); }
void meow_gfx_ft_dirty(int x, int y, int w, int h)
{ dirty_add(x, y, w, h); }
/* ================= RGBA blit(图片层用) ================= */
void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha)
{
@@ -833,6 +856,106 @@ void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alp
dirty_add(x1, y1, x2 - x1, y2 - y1);
}
/* ================= RGB565 页图(M32) =================
* 阅读器页槽常驻 RGB565(2B/px)而不是 RGBA:真机 480×853 页 1599KB → 800KB。
* 量化与 back_buf 用同一个 rgb565()(逐位截断),所以 565 屏上落下的像素与旧的
* RGBA blit 路径**逐像素一致**——不引入色偏,也不会比现在多出色带。
* 命中页(解码/落框尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次 memcpy,
* 单次拷贝、没有每帧重采样。alpha 按不透明处理(页图 alpha 恒为 255;带透明像素的
* PNG/32bpp BMP 页会与旧 RGBA 路径不同——旧路径混合到底色,这里直接画 RGB)。 */
uint16_t meow_gfx_rgb565(uint32_t rgb)
{
return rgb565(rgb);
}
void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx)
{
if (!rgba || !out || npx <= 0) return;
for (int i = 0; i < npx; i++) {
const uint8_t *p = rgba + (size_t)i * 4;
out[i] = rgb565(((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]);
}
}
/* 目标矩形与 clip 求交;返回 1=还有可画区域。 */
static int blit565_clip(int x, int y, int w, int h, int *x1, int *y1, int *x2, int *y2)
{
if (!g_init_ok || w <= 0 || h <= 0) return 0;
*x1 = x; *y1 = y; *x2 = x + w; *y2 = y + h;
if (*x1 < 0) *x1 = 0;
if (*y1 < 0) *y1 = 0;
if (*x2 > g_xres) *x2 = g_xres;
if (*y2 > g_yres) *y2 = g_yres;
if (g_clip_n > 0) {
int *c = g_clip_stack[g_clip_n - 1];
if (*x1 < c[0]) *x1 = c[0];
if (*y1 < c[1]) *y1 = c[1];
if (*x2 > c[2]) *x2 = c[2];
if (*y2 > c[3]) *y2 = c[3];
}
return *x2 > *x1 && *y2 > *y1;
}
void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px)
{
if (!px) return;
int x1, y1, x2, y2;
if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return;
int n = x2 - x1;
for (int py = y1; py < y2; py++) {
const uint16_t *src = px + (size_t)(py - y) * w + (x1 - x);
unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8);
if (g_bpp == 16) {
memcpy(dst, src, (size_t)n * 2); /* 命中页:每行一次拷贝 */
} else {
uint32_t *d32 = (uint32_t *)dst;
for (int i = 0; i < n; i++) {
uint16_t c = src[i];
d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) |
((uint32_t)ex6((c >> 5) & 0x3F) << 16) |
((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu;
}
}
}
dirty_add(x1, y1, n, y2 - y1);
}
/* 最近邻缩放(仅几何不符时的兜底路径;采样点与 meow_img_resample 同式)。
* 命中页不会走到这里——那是热路径纪律(M17/M32)。 */
void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h,
int sw, int sh, const uint16_t *px)
{
if (!px || sw <= 0 || sh <= 0) return;
int x1, y1, x2, y2;
if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return;
int n = x2 - x1;
for (int py = y1; py < y2; py++) {
int sy = (int)((long long)(py - y) * sh / h);
if (sy >= sh) sy = sh - 1;
const uint16_t *row = px + (size_t)sy * sw;
unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8);
if (g_bpp == 16) {
uint16_t *d16 = (uint16_t *)dst;
for (int i = 0; i < n; i++) {
int sx = (int)((long long)(x1 + i - x) * sw / w);
if (sx >= sw) sx = sw - 1;
d16[i] = row[sx];
}
} else {
uint32_t *d32 = (uint32_t *)dst;
for (int i = 0; i < n; i++) {
int sx = (int)((long long)(x1 + i - x) * sw / w);
if (sx >= sw) sx = sw - 1;
uint16_t c = row[sx];
d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) |
((uint32_t)ex6((c >> 5) & 0x3F) << 16) |
((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu;
}
}
}
dirty_add(x1, y1, n, y2 - y1);
}
/* ================= present / 上屏 ================= */
static void dump_ppm(void)
{
@@ -865,7 +988,7 @@ static void statusbar_collapse_step(void)
int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0;
if (g_sbar_collapse_h == tgt) return;
if (g_sbar_collapse_anim_ms <= 0) { g_sbar_collapse_h = tgt; return; }
unsigned long long now = now_ms();
unsigned long long now = meow_ui_time_ms();
unsigned long long el = now - g_sbar_collapse_start_ms;
int span = g_sbar_collapse_anim_ms;
if ((int)el >= span) { g_sbar_collapse_h = tgt; return; }
@@ -902,7 +1025,7 @@ void meow_ui_statusbar_set_collapsed(int on)
if (tgt == g_sbar_collapsed) return;
g_sbar_collapsed = tgt;
g_sbar_collapse_from_h = g_sbar_collapse_h;
g_sbar_collapse_start_ms = now_ms();
g_sbar_collapse_start_ms = meow_ui_time_ms();
dirty_full(); /* 顶栏显隐/上收变化 → 全帧重画 */
meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合,agent-146) */
}
@@ -944,13 +1067,33 @@ static int statusbar_touched(void)
return 0;
}
/* ---- §10.5 叠加让位自抑(present 层)----
* /tmp/meow_notif_active 存在 = 通知中心叠加会话进行中(launcher 正在 fb 上渲染面板)。
* App 进程(非系统 UI 渲染者)此时不上屏;200ms 节流缓存存在性(别每帧 stat)。
/* ---- §10.5 叠加让位自抑(present / input 共用判定)----
* /tmp/meow_notif_active = 前台屏幕归属标记,**内容=当前前台 app_id**(空=菜单/叠加层覆盖/
* 无前台单元,即「抑制全部 App」)。App 进程(非系统 UI 渲染者)据此判「我是否前台」:
* - 内容==本进程 app_id(env MEOW_LOG_APP)→ 前台 → 照常上屏 / 收输入;
* - 内容为空 → 菜单前台或被叠加层覆盖 → 全抑制(含自身);
* - 内容为其它 app_id → 后台常驻(批①多 App)/非前台 → 自抑上屏 + 排空输入。
* 200ms 节流缓存存在性+内容(别每帧 open/stat)。
* 计数 g_overlay_suppressed 供宿主单测断言(meow_gfx_overlay_suppressed_count)。 */
static unsigned long long g_overlay_suppressed = 0;
/* §后台化保活-残帧:记录「上一次 present 是否处于叠加自抑态」,用于在抑制→不抑制的恢复边沿
* 强制整帧重画(盖掉 launcher 在后台化期间画进 buffer0 的桌面/主界面帧)。 */
static int g_was_overlay_suppressed = 0;
static int overlay_present_suppressed(void)
/* 本进程 app_id(env MEOW_LOG_APP,launcher 注入)= 判「我是否前台」的身份锚。 */
static const char *overlay_own_app(void)
{
static const char *s = NULL;
if (!s) {
const char *e = getenv("MEOW_LOG_APP");
s = (e && e[0]) ? e : "";
}
return s;
}
/* §批①多 App 后台常驻:present 层与 input 层共用的「是否自抑」判定(单一实现,防两端漂移)。
* 系统 UI(launcher 渲染面板/主界面)恒 0。返回 1=本进程应自抑上屏+排空输入。 */
int meow_ui_overlay_suppressed_active(void)
{
if (meow_ui_sys_render_enabled()) return 0; /* launcher(系统 UI)渲染面板,照常上屏 */
static char s_path[108];
@@ -962,10 +1105,32 @@ static int overlay_present_suppressed(void)
snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : MEOW_NOTIF_ACTIVE_DEFAULT);
s_path_init = 1;
}
unsigned long long now = now_ms();
unsigned long long now = meow_ui_time_ms();
if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res;
s_last_chk = now;
s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0;
{
char owner[48];
int have = 0;
owner[0] = 0;
int fd = open(s_path, O_RDONLY);
if (fd >= 0) {
ssize_t n = read(fd, owner, sizeof(owner) - 1);
close(fd);
if (n > 0) owner[n] = 0;
have = 1; /* 文件存在(无论内容)→ 处于抑制管控 */
} else {
have = 0; /* 文件不存在 → 无抑制 */
}
if (!have) {
s_last_res = 0;
} else if (owner[0] == 0) {
s_last_res = 1; /* 空内容=全抑制(菜单前台/叠加层覆盖) */
} else {
const char *me = overlay_own_app();
/* 内容==本进程 app_id → 前台不抑制;否则(后台常驻/非前台)→ 抑制 */
s_last_res = (strcmp(owner, me) == 0) ? 0 : 1;
}
}
return s_last_res;
}
@@ -976,13 +1141,25 @@ static void present(void)
if (!g_init_ok || !g_back_buf) return;
if (!(g_dirty_full || g_ndirty > 0)) return;
/* §10.5 叠加让位自抑(仅 App 进程):App 前台播放中唤起通知中心时 launcher 不 SIGSTOP
* (保 PCM 供流,App 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf,
* 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层。系统 UI 进程(launcher 渲染面板)
* 不抑制。抑制期脏区保留累积(超上限自然升级整帧),标记消失后下一次 present 一次上屏。 */
if (overlay_present_suppressed()) { g_overlay_suppressed++; return; }
* (保 PCM 供流,ncm 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf,
* 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层;后台常驻(批①)App 同判据自抑。
* 系统 UI 进程(launcher 渲染面板)不抑制。抑制期脏区保留累积(超上限自然升级整帧),
* 判定为「回到前台」后下一次 present 一次上屏。 */
if (meow_ui_overlay_suppressed_active()) { g_overlay_suppressed++; g_was_overlay_suppressed = 1; return; }
/* §后台化保活-残帧:抑制→不抑制的**恢复边沿**强制整帧重画。App 被 launcher 转后台保活
* (/tmp/meow_notif_active 存在)期间 present 被自抑,back_buf 仍保留 App 自己的完整帧;
* 但 launcher 已把桌面/主界面整帧画进 buffer0 盖在 App 残帧上。恢复前台时若只 present
* 抑制期累积的小脏区(时钟/状态栏),launcher 的桌面帧会在 App 未重画区域透出(真机
* 「桌面叠在 ncm UI 下面/旧播放界面层叠」。标记由有→无即「回到前台」,强制 dirty_full
* 让下一次 present 把 App 完整 back_buf 一次上屏,盖掉桌面帧。仅真实 App 进程(launcher
* 系统 UI 恒不抑制,本边沿不触发)。 */
if (g_was_overlay_suppressed) {
g_was_overlay_suppressed = 0;
dirty_full();
}
/* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现;
* 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */
unsigned long long now = now_ms();
unsigned long long now = meow_ui_time_ms();
int period = (g_eff_rate > 0) ? (1000 / g_eff_rate) : 16;
if (!g_refresh_always_max) {
if (g_last_present_ms && now - g_last_present_ms < (unsigned long long)period) return;
@@ -1012,7 +1189,7 @@ static void present(void)
}
if (g_fb_mem) {
ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */
unsigned long long tbg = now_ms();
unsigned long long tbg = meow_ui_time_ms();
/* 「同一个状态栏」闭环:App 进程不渲染状态栏(meow_ui_sys_render_enabled()==0),也绝不向
* 真实 fb 写顶条 inset 区(顶条由 launcher 系统进程叠加绘制)。否则 App 首帧全量拷贝
* (meow_ui_init 置 dirty_full)会把 launcher 刚叠画的顶条冲成未初始化数据——真机
@@ -1049,7 +1226,7 @@ static void present(void)
}
}
}
refresh_governor(now_ms() - tbg, period);
refresh_governor(meow_ui_time_ms() - tbg, period);
}
g_last_present_ms = now;
g_frames_presented++;
+11 -11
View File
@@ -362,10 +362,10 @@ static const char *py_key_label(const struct kbkey *k, char *tmp)
switch (k->kind) {
case K_CH: { char c = (char)tolower((unsigned char)k->ch); tmp[0] = c; tmp[1] = 0; return tmp; }
case K_BACK: return "⌫";
case K_SPACE: return "空格";
case K_ENTER: return "确定";
case K_SWITCH: return "拼/英";
case K_SYMBOL: return "?123";
case K_SPACE: return meow_str("ime.space");
case K_ENTER: return meow_str("ime.enter");
case K_SWITCH: return meow_str("ime.switch");
case K_SYMBOL: return meow_str("ime.symbol");
default: return "";
}
}
@@ -398,9 +398,9 @@ static void meow_ime_draw_py(const meow_theme *th)
snprintf(pybuf, sizeof pybuf, "%s_", g_py_buf);
meow_ui_text(14, PY_COMBO_Y + 8, pybuf, th->font_body, th->onSurface);
meow_gfx_fill_round(PY_CLEAR_X, PY_COMBO_Y + 4, PY_CLEAR_W, PY_BADGE_H, 8, th->secondaryContainer);
meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, "清", 20, th->onSecondaryContainer);
meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, meow_str("ime.clear"), 20, th->onSecondaryContainer);
meow_gfx_fill_round(PY_BADGE_X, PY_BADGE_Y, PY_BADGE_W, PY_BADGE_H, 8, th->primaryContainer);
meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, "拼音", 20, th->onPrimaryContainer);
meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, meow_str("ime.pinyin_mode"), 20, th->onPrimaryContainer);
/* 候选栏:◀ ▶ 翻页 + 横排候选(高亮第 0 项) */
meow_ui_fill_rect(0, PY_CAND_Y, 480, PY_CAND_H, th->surfaceContainerHighest);
@@ -409,7 +409,7 @@ static void meow_ime_draw_py(const meow_theme *th)
meow_gfx_fill_round(PY_NEXT_X, PY_CAND_Y, PY_NEXT_W, PY_CAND_H, 8, th->surfaceContainerLow);
meow_ui_text_center(PY_NEXT_X + PY_NEXT_W / 2, PY_CAND_Y + (PY_CAND_H - th20) / 2, "▶", 20, th->onSurface);
if (g_py_nc == 0) {
meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, "(无候选)", th->font_body, th->onSurfaceVariant);
meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, meow_str("ime.no_candidate"), th->font_body, th->onSurfaceVariant);
} else {
int x = PY_CAND_X0;
for (int i = 0; i < PY_PAGE; i++) {
@@ -458,7 +458,7 @@ void meow_ime_draw(void)
{
int th20 = meow_ui_text_height(20);
meow_gfx_fill_round(ASCII_MID_X, KB_TOP + 7, ASCII_MID_W, 28, 8, th->primaryContainer);
meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, "中", 20, th->onPrimaryContainer);
meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, meow_str("ime.to_pinyin"), 20, th->onPrimaryContainer);
}
/* 键 */
for (int i = 0; i < g_nkeys; i++) {
@@ -471,9 +471,9 @@ void meow_ime_draw(void)
char label[2] = { k->ch, 0 };
if (is_letter(k->ch)) label[0] = g_shift ? (char)toupper((unsigned char)k->ch)
: (char)tolower((unsigned char)k->ch);
if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "SPACE", th->font_body, fg); }
else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "OK", 20, fg); }
else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, "X", 20, fg); }
if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.space"), th->font_body, fg); }
else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.enter"), 20, fg); }
else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.esc"), 20, fg); }
else { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, label, 20, fg); }
}
}
+180 -7
View File
@@ -1,10 +1,11 @@
/*
* meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入),
* 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。
* meow_ui_img.c — 图片层(B1.2)。解码:M23 真缩解后端(meow_jpeg.c / meow_png_stream.c)
* 优先,不支持/失败回退 stb_image(单头文件、静态编入);统一输出 RGBA,最近邻缩放/居中裁方。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "stb_image.h"
#include "meow_jpeg.h" /* M23 真缩解后端(内部头,不对外导出) */
#include <stdio.h>
#include <stdlib.h>
@@ -42,6 +43,147 @@ meow_img *meow_img_load_file(const char *path)
return img_alloc(w, h, p);
}
/* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档,
* 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim
* 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */
int meow_img_pick_scale(int longside, int maxdim)
{
int s = 1;
while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2;
return s;
}
/* 追加(M12):缩解结果落地 + 需要时精修。p 所有权转交。 */
static meow_img *img_finish_dim(unsigned char *p, int w, int h, int maxdim)
{
if (!p) return NULL;
meow_img *im = img_alloc(w, h, p);
if (!im) return NULL;
if (im->w <= maxdim && im->h <= maxdim) return im;
meow_img *small = meow_img_downscale(im, maxdim);
if (!small) return im; /* 精修失败:缩解结果已小于原图,仍可用 */
meow_img_free(im);
return small;
}
int meow_img_probe_file(const char *path, int *w, int *h, int *channels)
{
int pw = 0, ph = 0, pc = 0;
if (!path || !stbi_info(path, &pw, &ph, &pc)) return 0;
if (w) *w = pw;
if (h) *h = ph;
if (channels) *channels = pc;
return 1;
}
meow_img *meow_img_load_file_dim(const char *path, int maxdim)
{
if (!path) return NULL;
if (maxdim <= 0) return meow_img_load_file(path); /* 不限尺寸 = 旧入口 */
/* 追加(M23):真缩解后端直接产出目标尺寸(JPEG 按 DCT 档位;PNG 逐行 inflate 丢行丢列;
* BMP 按行 seek 跳采样)。峰值只与输出尺寸相关。
* 不支持/失败/几乎等尺寸(4 分量 CMYK、算术编码、交错 PNG、RLE BMP、小图…)返回 0 →
* 落回下面的 stb 旧路径,对外语义与档位表现不变。 */
{
unsigned char *bp = NULL;
int bw = 0, bh = 0;
if (meow_img_backend_file(path, maxdim, &bw, &bh, &bp) && bp)
return img_finish_dim(bp, bw, bh, maxdim);
}
int w = 0, h = 0, c = 0;
if (!stbi_info(path, &w, &h, &c)) return NULL;
int longside = w > h ? w : h;
if (longside <= maxdim) return meow_img_load_file(path); /* 小图不动:旧路径 */
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, meow_img_pick_scale(longside, maxdim));
return img_finish_dim(p, w, h, maxdim);
}
/* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim,
* 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。
* M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶,
* 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。
*
* M27 r1(P1-1):allow_stb_fallback=0 时只尝试后端,**绝不回退 stb**——阅读器的门控按
* 「后端峰值」放行时必须走这里,否则「头部说支持、解码中途失败」会静默落到 stb,
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG),即 M22 的 SIGKILL 场景。
* 后端失败返回 NULL(调用方按坏图占位)。 */
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
int allow_stb_fallback, int *used_backend)
{
if (used_backend) *used_backend = 0;
if (!path) return NULL;
if (step <= 1 && maxdim <= 0)
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
if (step <= 0) step = 1;
if (step > 8) step = 8;
int w = 0, h = 0, c = 0;
if (!stbi_info(path, &w, &h, &c)) return NULL;
int longside = w > h ? w : h;
if (step == 1 && maxdim <= 0)
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
/* 追加(M23/M27):真缩解后端优先(JPEG/PNG/BMP 全部格式)。step 是「输出别比这个档位
* 更细」的提示:折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),
* 与下面 stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错/RLE…)时回退 stb,行为与 M22 一致。
* M27 起阅读器的峰值模型已与后端同口径(page_source 的 reader_estimate_peak),
* 所以 PNG/BMP 也一并接后端——这正是「修好了却用不上」的最后一处缝。 */
{
int eff = maxdim > 0 ? maxdim : longside;
int step_cap = (longside + step - 1) / step;
unsigned char *bp = NULL;
int bw = 0, bh = 0;
if (step_cap > 0 && step_cap < eff) eff = step_cap;
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp) {
if (used_backend) *used_backend = 1;
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
}
}
if (!allow_stb_fallback) return NULL; /* P1-1:门控按后端放行的页,失败即坏图 */
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step);
/* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */
return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside);
}
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
{
/* 通用入口保持 M22 语义:后端失败允许回退 stb。 */
return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL);
}
/* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性
* 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover);
meow_img *meow_img_resample(meow_img *im, int w, int h)
{
if (!im || w <= 0 || h <= 0) return NULL;
if (im->w == w && im->h == h) return NULL;
unsigned char *tmp = scale_buf(im, w, h, 0);
if (!tmp) return NULL;
return img_alloc(w, h, tmp);
}
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim)
{
if (!buf || len <= 0) return NULL;
if (maxdim <= 0) return meow_img_load_mem(buf, len);
/* 追加(M23):同文件入口,后端直接从调用方缓冲解码(不再整幅解 + 重采样)。 */
{
unsigned char *bp = NULL;
int bw = 0, bh = 0;
if (meow_img_backend_mem(buf, (size_t)len, maxdim, &bw, &bh, &bp) && bp)
return img_finish_dim(bp, bw, bh, maxdim);
}
int w = 0, h = 0, c = 0;
if (!stbi_info_from_memory((const unsigned char *)buf, len, &w, &h, &c)) return NULL;
int longside = w > h ? w : h;
if (longside <= maxdim) return meow_img_load_mem(buf, len);
unsigned char *p = stbi_load_scaled_from_memory((const unsigned char *)buf, len,
&w, &h, &c, 4,
meow_img_pick_scale(longside, maxdim));
return img_finish_dim(p, w, h, maxdim);
}
void meow_img_free(meow_img *im)
{
if (!im) return;
@@ -49,8 +191,12 @@ void meow_img_free(meow_img *im)
free(im);
}
int meow_img_w(meow_img *im) { return im ? im->w : 0; }
int meow_img_h(meow_img *im) { return im ? im->h : 0; }
int meow_img_w(const meow_img *im) { return im ? im->w : 0; }
int meow_img_h(const meow_img *im) { return im ? im->h : 0; }
/* 访问器:取 RGBA 像素指针(meow_img 对外 opaque,本函数为唯一像素读入口;供取色/直读像素)。
* NULL 返回 NULL。 */
const unsigned char *meow_img_pixels(const meow_img *im) { return im ? im->rgba : NULL; }
/* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
@@ -70,13 +216,16 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */
float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */
float oys = ((float)im->h - (float)dh * s) * 0.5f;
if (oxs < 0) oxs = 0; if (oys < 0) oys = 0;
if (oxs < 0) oxs = 0;
if (oys < 0) oys = 0;
for (int dy = 0; dy < dh; dy++) {
int syi = (int)((float)dy * s + oys);
if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1;
if (syi < 0) syi = 0;
if (syi >= im->h) syi = im->h - 1;
for (int dx = 0; dx < dw; dx++) {
int sxi = (int)((float)dx * s + oxs);
if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1;
if (sxi < 0) sxi = 0;
if (sxi >= im->w) sxi = im->w - 1;
memcpy(out + ((size_t)dy * dw + dx) * 4,
im->rgba + ((size_t)syi * im->w + sxi) * 4, 4);
}
@@ -96,6 +245,30 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
return out;
}
/* 内存瘦身:把大图按最近邻等比缩放到「最长边 ≤ maxdim」,返回新图;调用方负责
* meow_img_free 旧图 + 新图。原图已 ≤maxdim → 返回 NULL(无需缩放)。
* 典型:网易云全尺寸封面 1448×1448 RGBA=8MB,显示只需 ≤480px(屏幕 480 宽)→
* 480×480=0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀)。
* 复用 scale_buf(cover=0,最近邻锁比例),不做插值(App 封面/背景无需高质量缩放)。 */
meow_img *meow_img_downscale(meow_img *im, int maxdim)
{
if (!im || maxdim <= 0) return NULL;
if (im->w <= maxdim && im->h <= maxdim) return NULL; /* 已够小,不缩放 */
int dw, dh;
if (im->w >= im->h) {
dw = maxdim;
dh = (int)((long)im->h * maxdim / im->w);
} else {
dh = maxdim;
dw = (int)((long)im->w * maxdim / im->h);
}
if (dw < 1) dw = 1;
if (dh < 1) dh = 1;
unsigned char *tmp = scale_buf(im, dw, dh, 0);
if (!tmp) return NULL;
return img_alloc(dw, dh, tmp);
}
void meow_ui_img(int x, int y, meow_img *im)
{
if (!im) return;
+167 -20
View File
@@ -26,6 +26,8 @@
#include "meow_ui_sys.h"
#include "meow_ui_vol.h"
#include "meow_ui_media.h"
#include "meow_ui_view.h" /* M7.0a 缺口#7:把 meow_view_dispatch_event 接入 meow_ui_run */
#include "meow_ui_nav.h" /* 批②:底部上滑回桌面→meow_ui_nav_request_exit(MSG_NAV EXIT→批①保活) */
#include <stdio.h>
#include <stdlib.h>
@@ -56,17 +58,26 @@
#define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */
#define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */
#define HOME_EDGE_H 28 /* 批②:底部起滑区高(自屏底向上;镜像 np_gesture.h NP_HOME_EDGE_H) */
#define HOME_SWIPE 65 /* 批②:底部上滑位移阈值(镜像 np_gesture.h NP_HOME_SWIPE;= SWIPE_THRESH) */
#define MEOW_LONG_PRESS_MS 500u /* §6.3 通用长按默认阈值(ms;meow_ui_set_long_press_ms 可配) */
#define MEOW_LONG_PRESS_SLOP 12 /* §6.3 按下后「仍算按住」的最大位移(px);超此取消长按 */
#define QUEUE_N 128
#define MAX_DEVS 8
#define MEOW_APP_ACTIVITY_FILE "/tmp/meow_app_activity" /* M2.71-bug5:App 活动→launcher 重置息屏计时 */
/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */
void meow_ui_gesture_end(void);
/* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)──
* 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理
* (路由 meow_ui_sys_notif_pull),不下发给 APP 的 on_swipe(顶部热区为系统保留手势)。
* 与左缘返回(SWIPE_LEFT)/ meow_ui_swipe_hint 互不影响——后者仍全屏生效,详见下方触摸机。 */
#define MEOW_EV_NOTIF_PULL 5
/* 批②:底部热区上滑 → 回桌面(后台保活,走批①链路)。系统手势,a=MEOW_SWIPE_UP(供
* on_swipe 接管、view 树消费),b=位移(px)。定义在 input.c 内部(同 MEOW_EV_NOTIF_PULL
* 未入公共头),宿主单测按同值声明(见 test_meow_ui_home_gesture.c)。 */
#define MEOW_EV_HOME 9
/* 顶部下滑 → 通知中心判定:纵向 DOWN 手势的起点(origin_y)是否落在顶部热区内。
* 热区由 meow_ui_statusbar_hotzone() 给出(非全屏=状态栏高、全屏=MEOW_SBAR_HOTZONE_FS、
@@ -76,6 +87,20 @@ int meow_ui_input_is_notif_pull(int origin_y, int hotzone)
return origin_y >= 0 && origin_y < hotzone;
}
/* 批② 底部热区上滑 → 回桌面(home)判定:镜像 launcher np_gesture.h np_home_gesture 同口径
* (热区 28px / 上移 ≥65px / 竖向主导 / 避开左缘返回列),SDK 侧 fb_h=屏幕高(复用
* meow_ui_height())。供宿主单测直调(判据与触摸状态机 touch_release 共用)。本判定把
* 「底部热区起滑 + 上移超阈值且竖向主导」从普通上滑中剥离——中部上滑/滚动仍归 App
* (LazyColumn/LazyGrid 滚动不受影响),底部热区的横向滑仍走原有左缘返回/横向逻辑。 */
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h)
{
int adx = dx < 0 ? -dx : dx;
if (down_x >= EDGE_W && down_y >= fb_h - HOME_EDGE_H && down_y >= 0 &&
dy <= -HOME_SWIPE && adx < -dy)
return 1;
return 0;
}
static int g_thread_started = 0;
static pthread_t g_thread;
static volatile int g_thread_run = 1;
@@ -136,28 +161,17 @@ static int meow_input_jack_debounce(void)
return g_jack_changed_at && (now - g_jack_changed_at) < 800;
}
/* ---- §10.5 叠加让位:通知中心叠加会话期输入排空(播放中 App 不被 SIGSTOP 的配套)----
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,App 播放线程同进程),
/* ---- §10.5 叠加让位:通知中心叠加会话期 / 批①后台常驻期 输入排空(播放中 App 不被 SIGSTOP 的配套)----
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,ncm 播放线程同进程),
* App 输入线程仍在读同一批 evdev 设备(每 fd 各收一份事件副本,与 launcher 互不剥夺)——若不
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发)。
* 故标记存在期:排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作
* 叠加期由 launcher/watchdog 单侧承担)。200ms 节流缓存存在性(与 present 层同口径,别每圈 stat)。 */
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发);
* 批①后台常驻 App 同理:前台 App(如 Hello)时,后台 ncm 的触摸必须被排空(一次交互只响应前台)。
* 判据统一走 meow_ui_overlay_suppressed_active()(内容=前台 app_id;非前台/空=抑制)。抑制期:
* 排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作叠加期由
* launcher/watchdog 单侧承担)。200ms 节流缓存由共享判定承担(与 present 层同口径)。 */
static int overlay_input_suppressed(void)
{
static char s_path[108];
static int s_path_init = 0;
static unsigned long long s_last_chk = 0;
static int s_last_res = 0;
if (!s_path_init) {
const char *e = getenv("MEOW_NOTIF_ACTIVE_FILE"); /* 宿主/单测覆写(进程内只解析一次) */
snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : "/tmp/meow_notif_active");
s_path_init = 1;
}
unsigned long long now = now_ms();
if (s_last_chk && now - s_last_chk < 200) return s_last_res;
s_last_chk = now;
s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0;
return s_last_res;
return meow_ui_overlay_suppressed_active();
}
/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)----
@@ -197,6 +211,7 @@ void meow_input_drain(void)
pthread_mutex_lock(&g_qm);
g_qh = 0; g_qt = 0;
pthread_mutex_unlock(&g_qm);
meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */
}
static void push_ev(int type, int a, int b)
@@ -332,7 +347,16 @@ static void touch_release(void)
}
} else {
int dir = mdy > 0 ? MEOW_SWIPE_DOWN : MEOW_SWIPE_UP;
push_ev(MEOW_EV_SWIPE, dir, ady > adx ? ady : adx);
int dist = ady > adx ? ady : adx;
/* 批②:竖向主导且上移(dir==UP)时,判「底部热区上滑 → 回桌面」。仅当起点在底部
* 热区(最后 HOME_EDGE_H px)且**不在左缘**(down_x>=EDGE_W,与左缘返回互斥)且
* 上移 ≥HOME_SWIPE 且竖向主导(adx<-dy)才命中;否则照常 SWIPE_UP(中部上滑/滚动交
* 由 App)。命中改入队 MEOW_EV_HOME(区别于普通上滑,供 run 循环按兜底回桌面)。 */
if (dir == MEOW_SWIPE_UP &&
meow_ui_input_is_home_swipe(t_dx, t_dy, mdx, mdy, meow_ui_height()))
push_ev(MEOW_EV_HOME, MEOW_SWIPE_UP, dist);
else
push_ev(MEOW_EV_SWIPE, dir, dist);
}
} else {
push_ev(MEOW_EV_TAP, t_cx, t_cy);
@@ -404,6 +428,89 @@ int meow_input_long_press_poll(void)
return 1;
}
/* ============================================================ */
/* §6.4 事件分发形式化:模态作用域 / 自上而下命中 / 手势捕获 / 唯一焦点
* ------------------------------------------------------------
* 统一仲裁者:把 ad-hoc 的「命中/捕获/消费/层间隔离/焦点」收敛为单一模型。
* 兼容:本节 API 只追加;未调用者行为与之前完全一致(命中/捕获仍由 App 自理)。 */
/* ============================================================ */
/* ---- 模态作用域栈(弹层叠开,depth 递增;存在则下层完全收不到触摸)---- */
#define MEOW_MODAL_SCOPE_MAX 8
static void *g_modal_stack[MEOW_MODAL_SCOPE_MAX];
static int g_modal_depth = 0;
static char g_modal_sentinel; /* 「模态哨兵」地址:模态存在时 hit_topdown 的返回占位 */
meow_ui_modal *meow_ui_modal_begin(void)
{
if (g_modal_depth >= MEOW_MODAL_SCOPE_MAX) return NULL; /* 栈满:不弹(骨架级静默) */
void *h = &g_modal_stack[g_modal_depth];
g_modal_stack[g_modal_depth++] = h;
return (meow_ui_modal *)h;
}
void meow_ui_modal_end(meow_ui_modal *m)
{
if (!m) return;
for (int i = 0; i < g_modal_depth; i++)
if (g_modal_stack[i] == (void *)m) {
g_modal_depth = i; /* 弹到 m(含),一次性关上嵌套上层 */
return;
}
}
int meow_ui_modal_active(void) { return g_modal_depth > 0; }
int meow_ui_modal_depth(void) { return g_modal_depth; }
/* ---- 自上而下命中(模态门控 + z 降序控件命中)---- */
void *meow_ui_hit_topdown(int x, int y)
{
if (meow_ui_modal_active()) return &g_modal_sentinel; /* 模态存在:恒由模态/背板消费,不透传 */
return meow_draw_hit_topdown(x, y);
}
int meow_ui_widget_hit(meow_widget *w, int x, int y) { return meow_widget_hit(w, x, y); }
/* ---- 手势捕获(down 捕获直到 up)---- */
static void *g_capture = NULL;
void *meow_ui_gesture_begin(int x, int y)
{
void *t = meow_ui_hit_topdown(x, y);
g_capture = t; /* 覆盖旧捕获(一次手势至多一个目标;命中 NULL=未捕获,move/up 照旧) */
return g_capture;
}
void *meow_ui_gesture_owner(void) { return g_capture; }
void meow_ui_gesture_end(void) { g_capture = NULL; }
/* ---- 唯一焦点(list 焦点残留根治 / 滚动容器间转移)---- */
static void *g_focus = NULL;
static meow_ui_focus_lost_fn g_focus_lost = NULL;
static void *g_focus_lost_ctx = NULL;
void meow_ui_focus_set_lost_cb(meow_ui_focus_lost_fn cb, void *ctx)
{ g_focus_lost = cb; g_focus_lost_ctx = ctx; }
void *meow_ui_focus_owner(void) { return g_focus; }
int meow_ui_focus_acquire(void *owner)
{
if (!owner) return 0;
void *prev = g_focus;
g_focus = owner;
if (prev && prev != owner && g_focus_lost) g_focus_lost(prev, g_focus_lost_ctx);
return 1;
}
int meow_ui_focus_release(void *owner)
{
if (!g_focus) return 0;
if (owner && owner != g_focus) return 0;
void *prev = g_focus;
g_focus = NULL;
if (g_focus_lost) g_focus_lost(prev, g_focus_lost_ctx);
return 1;
}
void meow_ui_focus_tap(int x, int y)
{
if (!g_focus) return;
void *hit = meow_ui_hit_topdown(x, y);
if (hit != g_focus) meow_ui_focus_release(g_focus); /* 点非焦点区 → 释放并通知 */
}
/* ---- 系统键(音量/媒体)去重 + EVIOCGKEY 状态轮询 ----
* M2.55/agent-217 之后 SDK 在输入线程消化系统键语义;但真机物理键(jz adc keyboard event2、
* md-gpio-keys event0、earpods_adc event3)的按下不一定进入 evdev 事件流(事件流可被其它进程
@@ -620,11 +727,21 @@ void meow_ui_run(const meow_ui_events *ev)
meow_ui_swipe_hint(-1);
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
/* §6.4 手势结束(抬手边沿)+ 焦点点外释放:先释放手势捕获(move/up 已结束),
* 再对落点做「命中焦点所有者→保位;否则释放并通知」(list 焦点残留根治)。 */
meow_ui_gesture_end();
meow_ui_focus_tap(e.a, e.b);
/* §6.1 触摸分流:③ 系统全局(IME/状态栏…已在上面消费) → ② App 全局控件
* (命中返回 1=消费,否则落到 ① active Activity 页面控件)。 */
if (meow_ui_global_hit(e.a, e.b)) continue;
/* M7.0a 缺口#7:meow_view 事件路由(§A.7)——有活动页 view 子树且其 on_event
* 命中即消费(不再落 App on_tap);无 view 树/未命中返回 0 → 回落 on_tap 原路径。 */
if (meow_view_dispatch_event(MEOW_EV_TAP, e.a, e.b)) continue;
if (g_events_set && g_events.on_tap) g_events.on_tap(e.a, e.b);
} else if (e.type == MEOW_EV_KEY) {
/* M7.0a 缺口#7:先进 view 路由,命中即消费;否则回落 App on_key(语义不变——
* 音量/媒体系统键已在输入线程消化,这里只决定 App/页面是否收到语义事件)。 */
if (meow_view_dispatch_event(MEOW_EV_KEY, e.a, e.b)) continue;
if (g_events_set && g_events.on_key) g_events.on_key(e.a, e.b);
} else if (e.type == MEOW_EV_SWIPE) {
if (in_startup) continue; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */
@@ -633,6 +750,10 @@ void meow_ui_run(const meow_ui_events *ev)
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
/* 拼音 IME 候选栏横滑翻页(英文模式 handle_event 返回 0 → 沿用既有行为)。 */
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
meow_ui_gesture_end(); /* §6.4:抬手(滑动)边沿,释放手势捕获 */
/* M7.0a 缺口#7:view 路由先于 App 窗口的 on_swipe 与默认返回手势——页 view
* 可消费滑动(页内返回/翻页/横幅);命中即跳过下面对 on_swipe/默认退出的分发。 */
if (meow_view_dispatch_event(MEOW_EV_SWIPE, e.a, e.b)) continue;
/* A-0 全局左缘返回(方案A):
* - APP 注册 on_swipe → APP 接管,自行决定(meow_ui_request_exit / 页内返回);
* - APP 未注册 on_swipe 且非全屏(sb_active)→ SDK 默认退出 app(返回=退出);
@@ -644,6 +765,22 @@ void meow_ui_run(const meow_ui_events *ev)
} else if (e.a == MEOW_SWIPE_LEFT && sb_active) {
meow_ui_request_exit();
}
} else if (e.type == MEOW_EV_HOME) {
/* 系统级「底部热区上滑回桌面」(用户 2026-09-04 批②补全,任意 App 内生效):
* 屏底边缘热区+竖向主导上滑已由 meow_ui_input_is_home_swipe 过滤(与 App 中部
* 滚动、底部横向滑、左缘返回互斥),故作为**系统保留手势**优先于 App on_swipe /
* view 树——否则现有 App(ncm/hello/files/reader 均注册 on_swipe)会接管导致
* App 内上滑不回桌面。命中调 meow_ui_nav_request_exit()(MSG_NAV EXIT → launcher
* 批① meow_bg_add 保活,进程保留转后台、SDK present 自抑);**不**调
* meow_ui_request_exit()——那会断送保活(进程 exit(0))。launcher 据 app 声明
* (background_keepalive/background_audio+媒体活跃)决定无限期/宽限。 */
if (in_startup) continue; /* 启动窗口误触残留:丢弃 */
meow_ui_swipe_hint(-1);
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
/* IME 打开时软键盘覆盖底部:键盘拥有底部区,把事件交给 IME 且**绝不**回桌面
* (防输入时误退)。 */
if (meow_ime_active()) { (void)meow_ime_handle_event(&e); continue; }
meow_ui_nav_request_exit();
} else if (e.type == MEOW_EV_NOTIF_PULL) {
if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */
/* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发
@@ -652,6 +789,7 @@ void meow_ui_run(const meow_ui_events *ev)
* 起点若同时落在左缘,拖动期可能已画过 MEOW_EV_SWIPE_PROGRESS 左缘指示条,
* 抬手定判为 NOTIF_PULL 后须清掉(否则通知中心打开后残留返回条)。 */
meow_ui_swipe_hint(-1);
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
meow_ui_sys_notif_pull(e.a); /* e.a = 手势按下原点 Y */
} else if (e.type == MEOW_EV_TOUCH_DOWN || e.type == MEOW_EV_TOUCH_MOVE) {
/* M2.71-bug2/6:拖动过程(跟手)。IME 弹出时触摸优先喂键盘(键盘覆盖层拦截,
@@ -660,6 +798,9 @@ void meow_ui_run(const meow_ui_events *ev)
* IME 打开时先把压点喂给拼音(候选栏滑动翻页判定 g_ime_swipe_y),再丢弃。 */
if (in_startup) continue;
if (meow_ime_active()) { meow_ime_handle_event(&e); continue; }
/* §6.4:down 捕获——命中目标捕获整个手势直到 up;随后 move/up 只发给该目标
* (防滑出后误触发下层)。真实触发放置捕获(宿主直调 on_touch_* 不受影响)。 */
if (e.type == MEOW_EV_TOUCH_DOWN) meow_ui_gesture_begin(e.a, e.b);
if (g_events_set && g_events.on_touch_down && e.type == MEOW_EV_TOUCH_DOWN)
g_events.on_touch_down(e.a, e.b);
if (g_events_set && g_events.on_touch_move && e.type == MEOW_EV_TOUCH_MOVE)
@@ -686,6 +827,9 @@ void meow_ui_run(const meow_ui_events *ev)
if (in_startup) continue;
/* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
meow_ui_gesture_end(); /* §6.4:长按已触发,其抬手不再 TAP/SWIPE——顺手释放捕获 */
/* M7.0a 缺口#7:view 路由先于 §6.3 通用长按回调(页 view 可消费长按)。 */
if (meow_view_dispatch_event(MEOW_EV_LONG_PRESS, e.a, e.b)) continue;
if (g_long_press_cb) g_long_press_cb(e.a, e.b);
}
}
@@ -697,6 +841,9 @@ void meow_ui_run(const meow_ui_events *ev)
g_events.on_tick();
}
}
/* §12.2 动画节拍器:推进所有活动动画(无动画时零开销)。挂既有 on_tick 机制,
* 不引入常驻高频轮询;动画 update 回调只 flush 其影响区(脏区联动)。 */
meow_anim_tick();
/* A-0 全局状态栏 1s 轻刷:电量/WiFi/音量 + 时钟。时钟每秒变 → 重画顶条并上屏,
* 即使 APP 空闲(未主动 flush)也保持时钟走动。
+26
View File
@@ -76,6 +76,10 @@ void meow_ui_sys_statusbar_render_off(int yoff);
/* 把第 idx 个 MD3 图标(sb_icons[])按 ink 等比 blit 进 back_buf;x=槽左缘、ink_h=目标 ink 高
* (0=自然 ink,WiFi 强弱档用)、fg=24bit 前景、cy=ink 垂直中心线。见 meow_ui_gfx.c。 */
void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy);
/* 同 meow_gfx_blit_icon,但**不标「顶栏」脏区**:供控件(W_TOPAPPBAR 位图动作图标)用——
* 控件矩形由 meow_widget_draw 的 clip 与 invalidation 独立管理,状态栏路径的顶栏脏区假设
* 不适用于位于屏幕内容区的 topbar。像素算法与状态栏同源(ink 居中 + alpha 软边混合)。 */
void meow_gfx_blit_icon_ink(int idx, int x, int ink_h, uint32_t fg, int cy);
/* ---- 顶级主题风格族(§8.1.2,minimal):状态栏图标集/配色随 ui.style(md3/miuix)----
* meow_ui_theme.c 实现:读 settings.conf 的 ui.style(md3→0 / miuix→1),缓存,随
@@ -127,6 +131,13 @@ void meow_gfx_mark_full(void);
* 菜单内容盖到 App 内容上)——叠加前用本函数撤销整帧脏,再按需标局部脏(agent-146)。 */
void meow_gfx_dirty_reset(void);
/* ---- 模态背景快照/恢复(meow_ui_dialog.c 提供;dialog/sheet/select 共用)----
* open 前调用 capture 把整帧背景拍下(modal_capture),close 后调用 restore 回填 + 整帧上屏
* (modal_restore)→ scrim 每次只叠一层、关闭即还原无残影(M2.71-bug3)。共享同一份快照缓冲,
* 各模态组件(meow_select 等)无需重复实现。 */
void meow_ui_modal_capture(void);
void meow_ui_modal_restore(void);
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
unsigned char *meow_ui_back_buf(void);
/* back_buf 的每行字节数(pitch)与位深(16/32)。M2.71-bug3:模态/IME 快照恢复用。 */
@@ -152,6 +163,12 @@ int meow_gfx_is_device(void);
* (仅 App 进程会自抑;宿主单测断言用,设备上无调用方)。 */
unsigned long long meow_gfx_overlay_suppressed_count(void);
/* §批①多 App 后台常驻:本进程是否应自抑上屏+排空输入(present/input 层共用判定)。
* /tmp/meow_notif_active 内容=前台 app_id:内容==本进程 app_id → 前台不抑制(返回 0);
* 内容为空或其它 app_id → 菜单前台/叠加层覆盖/后台常驻 → 自动抑制(返回 1)。
* 系统 UI(launcher)恒 0;200ms 节流缓存。meow_ui_gfx.c 实现,input 层复用。 */
int meow_ui_overlay_suppressed_active(void);
/* 中文/日文字库(mfont)是否已加载(状态栏「蓝牙」CJK 与回退 "BT" 二选一)。 */
int meow_gfx_mfont_ready(void);
@@ -234,6 +251,11 @@ void meow_draw_view_unregister(struct meow_view *v); /* view free:注
int meow_draw_view_entered(const struct meow_view *v); /* view 是否已进入(绘制过滤参考) */
void meow_draw_view_set_z(struct meow_view *v, int z); /* view 显式 z(z<=0=清除回退自动) */
/* §6.4 事件分发形式化:沿 z 序自顶向下命中(owner==active+enable 过滤;模态门控在
* meow_ui_hit_topdown,本函数只做 z 降序控件命中)。返回最高命中 z 的 meow_widget*,
* 无命中返回 NULL。实现见 meow_ui_draw.c(读绘制注册表,不改 struct meow_widget)。 */
void *meow_draw_hit_topdown(int x, int y);
/* 控件布局深度反推(meow_ui_draw.c 用;widget.c 实现,读 struct meow_widget 内部字段)。
* 让绘制注册表在「无 meow_view 时」仍能从 W_LAYOUT children 反推子>父深度。 */
int meow_widget_is_layout(const struct meow_widget *w); /* 1 = W_LAYOUT */
@@ -262,4 +284,8 @@ int meow_ime_handle_event(const meow_input_ev *e);
/* 当前 IME 是否拼音模式(供 meow_ui_input.c 决定是否喂长按轮询)。 */
int meow_ime_mode_pinyin(void);
/* ---- 时钟(meow_ui_time.c)---- */
/* 秒/纳秒 → 单调毫秒的唯一换算点(meow_ui_time_ms 与宿主单测共用)。 */
uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec);
#endif /* MEOW_UI_INTERNAL_H */
+2 -2
View File
@@ -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
View File
@@ -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)
+308
View File
@@ -0,0 +1,308 @@
/*
* meow_ui_scroll.c — 滚动物理(§12.3)。
*
* 速度采样:track 时把 (pos, t) 记进定长环形(≤MEOW_SCROLL_SAMPLES),fling 时取
* 最近 MEOW_SCROLL_WINDOW ms 窗口,按「间隔速度 × 近者权重」加权得出初速(px/s)。
* 位置模型:ease-out quad 的位移曲线(末速恰为 0),初速与输入 v 对齐,时长 = 2×tau。
* 边界:free 目标越界 → clamp 到 [0,max];若越界速度大 → 补两段短 IN_OUT 回弹
* (overshoot 超过边界方向的内侧再回位),不做弹簧。停止后可选 snap 到 step 倍数。
*
* 不引 libm(SDK 约定,免 -lm);只用整数 + 纯浮点四则。
*/
#include "meow_ui.h"
#include "include/meow_ui_scroll.h"
#include "meow_ui_internal.h" /* meow_widget_rect(定位 redraw 视口区) */
#include "include/meow_ui_anim.h"
#include <string.h>
#define MEOW_SCROLL_POOL 8
#define MEOW_SCROLL_SAMPLES 8 /* 速度采样点数(最近 N 加权) */
#define MEOW_SCROLL_WINDOW 110 /* 速度窗口(ms) */
#define MEOW_SCROLL_TAU 180 /* 默认摩擦时间常数(ms) */
#define MEOW_SCROLL_MIN_SPEED 60 /* 低于此的初速不 fling(px/s) */
#define MEOW_SCROLL_MAX_DUR 900 /* fling 时长上限(ms) */
#define MEOW_SCROLL_BOUNCE_MS 110 /* 每段回弹时长(ms) */
#define MEOW_SCROLL_SNAP_MS 120
typedef enum {
MEOW_SCROLL_IDLE = 0,
MEOW_SCROLL_FLING,
MEOW_SCROLL_BOUNCE1,
MEOW_SCROLL_BOUNCE2,
MEOW_SCROLL_SNAP,
} meow_scroll_phase;
struct meow_scroll {
meow_scroll_set_fn set;
meow_scroll_max_fn max;
meow_scroll_get_fn get;
meow_scroll_redraw_fn redraw;
void *ctx;
int snap_step; /* >0 = 吸附步长 */
int tau_ms; /* 摩擦时间常数 */
/* 速度采样(顺序数组,满则前移,≤SAMPLES) */
int v_pos[MEOW_SCROLL_SAMPLES];
uint64_t v_t[MEOW_SCROLL_SAMPLES];
int v_n;
meow_scroll_phase phase;
meow_anim *anim;
int maxv; /* fling 开始时缓存 max_scroll */
int bounce; /* fling 边界回弹幅度(带符号;0=不回弹) */
int bounce_start; /* 回弹回位起点(= clamp 边界) */
int snap_to; /* snap 目标 */
int wrap_slot; /* >=0:占用的 g_wrap_ctx 槽下标(-1=非 wrap 实例) */
};
static meow_scroll g_pool[MEOW_SCROLL_POOL];
static unsigned char g_used[MEOW_SCROLL_POOL];
/* wrap 上下文槽:meow_scroll_wrap 占用一个,meow_scroll_free 必须归还(否则进程内最多 wrap
* MEOW_SCROLL_POOL 次后恒 NULL)。槽由 wrap 实例独占,free 时按 wrap_slot 精确归还。 */
typedef struct { meow_widget *w; int lv; } wrap_ctx;
static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL];
static int g_wrap_used[MEOW_SCROLL_POOL];
/* ---- 生命周期 ---- */
meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
meow_scroll_get_fn get, meow_scroll_redraw_fn redraw,
void *ctx)
{
if (!set || !max || !get) return NULL;
for (int i = 0; i < MEOW_SCROLL_POOL; i++) {
if (!g_used[i]) {
g_used[i] = 1;
meow_scroll *s = &g_pool[i];
memset(s, 0, sizeof *s);
s->set = set; s->max = max; s->get = get; s->redraw = redraw; s->ctx = ctx;
s->tau_ms = MEOW_SCROLL_TAU;
s->wrap_slot = -1;
return s;
}
}
return NULL;
}
void meow_scroll_free(meow_scroll *s)
{
if (!s) return;
if (s->anim) meow_anim_cancel(s->anim);
s->anim = NULL;
for (int i = 0; i < MEOW_SCROLL_POOL; i++)
if (&g_pool[i] == s) {
int ws = s->wrap_slot;
if (ws >= 0 && ws < MEOW_SCROLL_POOL) { /* wrap 实例:归还上下文槽 */
g_wrap_ctx[ws].w = NULL;
g_wrap_ctx[ws].lv = 0;
g_wrap_used[ws] = 0;
}
s->wrap_slot = -1; /* 二次 free 时不再触碰任何槽 */
g_used[i] = 0;
return;
}
}
/* ---- wrap:包一个 meow_widget 列表 ---- */
static void wrap_set(void *c, int off)
{ wrap_ctx *wc = c; if (wc->lv) meow_listview_set_scroll(wc->w, off); else meow_list_set_scroll(wc->w, off); }
static int wrap_max(void *c)
{ wrap_ctx *wc = c; return wc->lv ? meow_listview_max_scroll(wc->w) : meow_list_max_scroll(wc->w); }
static int wrap_get(void *c)
{ wrap_ctx *wc = c; return wc->lv ? meow_listview_scroll(wc->w) : meow_list_scroll(wc->w); }
static void wrap_redraw(void *c)
{
wrap_ctx *wc = c;
if (!wc->w) return;
int r[4]; meow_widget_rect(wc->w, r);
if (r[2] > 0 && r[3] > 0) meow_ui_flush_region(r[0], r[1], r[2], r[3]); /* 只刷视口区 */
}
meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview)
{
if (!list) return NULL;
meow_scroll *s = meow_scroll_create(wrap_set, wrap_max, wrap_get, wrap_redraw, NULL);
if (!s) return NULL;
for (int i = 0; i < MEOW_SCROLL_POOL; i++) {
if (!g_wrap_used[i]) {
g_wrap_used[i] = 1;
g_wrap_ctx[i].w = list; g_wrap_ctx[i].lv = is_listview ? 1 : 0;
s->ctx = &g_wrap_ctx[i];
s->wrap_slot = i;
return s;
}
}
meow_scroll_free(s);
return NULL;
}
/* ---- 速度采样 ---- */
static void sample_push(meow_scroll *s, int pos, uint64_t t)
{
if (s->v_n >= MEOW_SCROLL_SAMPLES) { /* 满:前移丢弃最旧 */
for (int i = 1; i < s->v_n; i++) { s->v_pos[i - 1] = s->v_pos[i]; s->v_t[i - 1] = s->v_t[i]; }
s->v_n--;
}
s->v_pos[s->v_n] = pos; s->v_t[s->v_n] = t; s->v_n++;
}
/* 加权初速:取窗口内采样,按间隔速度 × 近者权重加权平均(px/s)。 */
static int sample_velocity(meow_scroll *s, uint64_t now)
{
int lo = 0;
while (lo < s->v_n && now > s->v_t[lo] && (now - s->v_t[lo]) > MEOW_SCROLL_WINDOW) lo++;
if (s->v_n - lo < 2) return 0;
long long sum_w = 0, sum_vw = 0;
for (int i = lo; i < s->v_n - 1; i++) {
uint64_t dt = s->v_t[i + 1] - s->v_t[i];
if (dt == 0) continue;
long long v = (long long)(s->v_pos[i + 1] - s->v_pos[i]) * 1000LL / (long long)dt;
long long w = (i - lo + 1); /* 近者权重高 */
sum_vw += v * w; sum_w += w;
}
if (sum_w == 0) return 0;
return (int)(sum_vw / sum_w);
}
int meow_scroll_velocity(meow_scroll *s)
{
if (!s) return 0;
return sample_velocity(s, meow_ui_time_ms());
}
void meow_scroll_track(meow_scroll *s, int dy, uint64_t t_ms)
{
if (!s || dy == 0) return;
int off = s->get(s->ctx) + dy; /* set 会 clamp 到 [0,max](与 scroll_by 一致) */
s->set(s->ctx, off);
int got = s->get(s->ctx); /* clamp 后的真实 offset */
sample_push(s, got, t_ms);
}
/* ---- 动画驱动回调 ---- */
static void fling_update(meow_anim *a, float v, void *ctx)
{
(void)a;
meow_scroll *s = (meow_scroll *)ctx;
s->set(s->ctx, (int)v);
if (s->redraw) s->redraw(s->ctx);
}
static void fling_done(meow_anim *a, int canceled, void *ctx)
{
(void)a;
meow_scroll *s = (meow_scroll *)ctx;
s->anim = NULL;
if (canceled) { s->phase = MEOW_SCROLL_IDLE; return; }
if (s->phase == MEOW_SCROLL_FLING) {
if (s->bounce != 0) {
/* 边界回弹段1:向内侧过冲 → 段2 回位(bounce 已带符号;顶界 +,底界 -) */
int from = s->get(s->ctx);
int to = from + s->bounce; /* bounce 已带符号 */
if (to < 0) to = 0;
if (to > s->maxv) to = s->maxv;
if (to == from) { s->phase = MEOW_SCROLL_IDLE; return; }
s->phase = MEOW_SCROLL_BOUNCE1;
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_BOUNCE_MS;
d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
return;
}
/* 无回弹 → snap 或收尾 */
if (s->snap_step > 0 && s->get(s->ctx) != s->snap_to) {
int from = s->get(s->ctx);
int to = s->snap_to;
if (to < 0) to = 0;
if (to > s->maxv) to = s->maxv;
if (to != from) {
s->phase = MEOW_SCROLL_SNAP;
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_SNAP_MS;
d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
return;
}
}
s->phase = MEOW_SCROLL_IDLE;
return;
}
if (s->phase == MEOW_SCROLL_BOUNCE1) {
int from = s->get(s->ctx);
/* 回位到 bounce 起点(= clamp 边界) */
int to = s->bounce_start;
if (to < 0) to = 0;
if (to > s->maxv) to = s->maxv;
s->phase = MEOW_SCROLL_BOUNCE2;
if (to == from) { s->phase = MEOW_SCROLL_IDLE; return; }
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)from; d.to = (float)to; d.duration_ms = MEOW_SCROLL_BOUNCE_MS;
d.ease = MEOW_EASE_IN_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
return;
}
if (s->phase == MEOW_SCROLL_BOUNCE2 || s->phase == MEOW_SCROLL_SNAP) {
s->phase = MEOW_SCROLL_IDLE;
return;
}
s->phase = MEOW_SCROLL_IDLE;
}
/* ---- fling ---- */
void meow_scroll_fling(meow_scroll *s)
{
if (!s) return;
if (s->anim) { meow_anim_cancel(s->anim); s->anim = NULL; }
s->phase = MEOW_SCROLL_IDLE;
int v = sample_velocity(s, meow_ui_time_ms());
if (v > -MEOW_SCROLL_MIN_SPEED && v < MEOW_SCROLL_MIN_SPEED) return; /* 太慢不 fling */
int max = s->max(s->ctx);
if (max < 0) max = 0;
s->maxv = max;
int pos0 = s->get(s->ctx);
int dist = (int)((long long)v * s->tau_ms / 1000LL); /* 摩擦滑行总位移(px) */
int free_target = pos0 + dist;
int clamped = free_target, over = 0;
if (clamped > max) { over = clamped - max; clamped = max; }
if (clamped < 0) { over = -clamped; clamped = 0; }
if (clamped == pos0) return; /* 无位移 */
int dir; /* 回弹方向:底界向 -1(往内上),顶界向 +1(往内下) */
if (free_target > max) dir = -1;
else if (free_target < 0) dir = 1;
else dir = 0;
int dur = 2 * s->tau_ms; /* ease-out quad:初速与 v 对齐,末速为 0 */
if (dur > MEOW_SCROLL_MAX_DUR) dur = MEOW_SCROLL_MAX_DUR;
s->bounce = 0;
s->snap_to = pos0;
if (s->snap_step > 0) {
int half = s->snap_step / 2;
s->snap_to = ((pos0 + half) / s->snap_step) * s->snap_step;
if (s->snap_to > max) s->snap_to = max;
}
if (over > 12) { /* 快速撞墙:clamp + 简单回弹 */
int amt = over / 10; if (amt < 3) amt = 3; if (amt > 14) amt = 14;
s->bounce = dir * amt; /* 带符号的向内幅度 */
/* 记录回位起点 = clamp 边界 */
s->bounce_start = clamped;
}
s->phase = MEOW_SCROLL_FLING;
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = (float)pos0; d.to = (float)clamped; d.duration_ms = (uint32_t)dur;
d.ease = MEOW_EASE_OUT; d.update = fling_update; d.done = fling_done; d.ctx = s;
s->anim = meow_anim_start(&d);
}
void meow_scroll_stop(meow_scroll *s)
{
if (!s) return;
if (s->anim) { meow_anim_cancel(s->anim); s->anim = NULL; }
s->phase = MEOW_SCROLL_IDLE;
}
int meow_scroll_active(meow_scroll *s) { return (s && s->phase != MEOW_SCROLL_IDLE) ? 1 : 0; }
int meow_scroll_offset(meow_scroll *s) { return s ? s->get(s->ctx) : 0; }
void meow_scroll_set_snap(meow_scroll *s, int step) { if (s) s->snap_step = step > 0 ? step : 0; }
void meow_scroll_set_friction(meow_scroll *s, int tau_ms)
{ if (s) s->tau_ms = (tau_ms > 20 && tau_ms <= 2000) ? tau_ms : MEOW_SCROLL_TAU; }
+288
View File
@@ -0,0 +1,288 @@
/*
* meow_ui_str.c — SDK 字符串资源框架(i18n 雏形,§13.meow_str_*)。
*
* 背景:SDK/系统 UI 文案此前全部硬编码在 C 源码(dialog 的 "OK/CANCEL"、IME 的 "空格/确定/
* 拼音/(无候选)" 等),第三方开发者无法按语言取串,公开 SDK 前必须外置。本 TU 提供
* 「按 locale 取整串」的最小框架:内嵌双语默认表(编译进 meow_ui.a,必可用)+ TF 卡覆盖表
* (MeowData/<pkg>/strings/<locale>.txt,开发者可加壳不改二进制)+ locale 存系统设置
* settings.conf 的 `ui.locale`(默认 zh_cn)。
*
* 取值优先级(meow_str,四层,逐层回落,永不返回 NULL):
* ① 当前 locale 的 TF 覆盖表(meow_ui_str_set_locale 加载,静态数组,≤256 条)
* ② 内嵌表中当前语言(en_us→en,其余→zh)
* ③ 内嵌表中内置默认(zh_cn,即 locale 缺省/未知语言时回退)
* ④ 仍不可得 → 原样返回 id(调用方可展示 id / 打点,绝不见 NULL)
*
* 设计要点:
* - 零 malloc 热路径:内嵌为编译期静态表;覆盖表为启动/切 locale 时的静态数组,
* meow_str 只做线性扫描(表 ≤256,O(n) 可接受),不申请任何堆。
* - 纯 libc:不依赖其它 SDK TU,宿主 gcc 直接可单测(同 meow_ui_config.c / meow_ui_vol.c)。
* - locale 初值惰性读 settings.conf ui.locale(默认 zh_cn);meow_str_set_locale 即时生效。
* - 覆盖表解析兼容 "key = value"('=' 两侧去空白),忽略空行/无 '=' 行;UTF-8 原样透传。
* - 幂等:settings 只读一次;覆盖表按需加载。locale 修改须在 App 主线程(防并发写覆盖表)。
*
* 边界(§13 明示不做):无复数规则/BiDi/RTL/动态字体回退,理由见 meow_ui_str.h。
*/
#include "meow_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#define MEOW_STR_SETTINGS_FILE "/usr/data/meow/settings.conf" /* 板载系统设置(与 theme/vol 同源) */
#define MEOW_STR_LOCALE_MAX 16
#define MEOW_STR_OVERRIDE_MAX 256 /* 覆盖表上限(§13 性能约束 ≤256) */
#define MEOW_STR_OVR_ID_MAX 64
#define MEOW_STR_OVR_TEXT_MAX 128
#define MEOW_STR_PATH_MAX 384
/* ---- 内嵌双语默认表(编译进二进制,必可用;zh = 内置默认)----
* id 规范:`域.名`(common.* 为 SDK 自带通用组;ime.* 为 SDK 自带 IME 键面组;
* 第三方 App 可自建 域(如 player.pause)经 TF 覆盖表追加,不改 SDK 二进制)。 */
typedef struct meow_str_embed {
const char *id;
const char *zh;
const char *en;
} meow_str_embed;
static const meow_str_embed g_embed[] = {
/* common.*:通用 UI(dialog 默认按钮 / 空态占位可复用) */
{ "common.ok", "确定", "OK" },
{ "common.cancel", "取消", "Cancel" },
{ "common.back", "返回", "Back" },
{ "common.confirm", "确认", "Confirm" },
{ "common.done", "完成", "Done" },
{ "common.save", "保存", "Save" },
{ "common.delete", "删除", "Delete" },
{ "common.edit", "编辑", "Edit" },
{ "common.search", "搜索", "Search" },
{ "common.input", "输入", "Input" },
{ "common.please_input", "请输入", "Please input" },
{ "common.select", "请选择", "Select" },
{ "common.loading", "加载中...", "Loading..." },
{ "common.no_data", "暂无数据", "No data" },
{ "common.retry", "重试", "Retry" },
{ "common.failed", "失败", "Failed" },
{ "common.retry_fail", "重试失败", "Retry failed" },
{ "common.close", "关闭", "Close" },
{ "common.unknown", "未知", "Unknown" },
{ "common.settings", "设置", "Settings" },
{ "common.tap_ok_edit", "点击确定编辑", "tap OK to edit..." },
/* ime.*:SDK 内置 IME 键面/徽标(meow_ui_ime.c 接入) */
{ "ime.space", "空格", "Space" },
{ "ime.enter", "确定", "OK" },
{ "ime.esc", "取消", "Cancel" },
{ "ime.switch", "拼/英", "PY/EN" },
{ "ime.symbol", "?123", "?123" },
{ "ime.clear", "清", "Clear" },
{ "ime.pinyin_mode", "拼音", "Pinyin" },
{ "ime.no_candidate", "(无候选)", "(none)" },
{ "ime.to_pinyin", "中", "中" },
};
#define MEOW_STR_EMBED_N (sizeof(g_embed) / sizeof(g_embed[0]))
/* ---- 运行时状态(静态,零 malloc)---- */
static char g_locale[MEOW_STR_LOCALE_MAX] = "zh_cn";
static int g_locale_loaded = 0; /* 1 = 已从 settings 读过一次 */
static int g_override_loaded = 0; /* 1 = 覆盖表已按当前 locale 加载 */
/* 覆盖表(当前 locale):线性静态数组(id/text 各自定长),≤ MEOW_STR_OVERRIDE_MAX。 */
static char g_ov_id[MEOW_STR_OVERRIDE_MAX][MEOW_STR_OVR_ID_MAX];
static char g_ov_text[MEOW_STR_OVERRIDE_MAX][MEOW_STR_OVR_TEXT_MAX];
static int g_ov_n = 0;
/* ---- settings.conf 读(同 meow_ui_theme.c get_setting_str 口径:'=' 切分,精确匹配 key)----
* 返回 0=命中并把值拷入 buf;-1=未命中/文件不可读/超长。 */
static int str_setting_get(const char *key, char *buf, int buflen)
{
FILE *f = fopen(MEOW_STR_SETTINGS_FILE, "r");
if (!f || buflen <= 0) return -1;
char line[160];
int kl = (int)strlen(key);
int found = 0;
while (fgets(line, sizeof line, f)) {
char *eq = strchr(line, '=');
if (!eq) continue;
char *k = line;
while (*k == ' ' || *k == '\t') k++;
int n = (int)(eq - k);
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--;
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue;
const char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++;
int m = 0;
while (*v && *v != '\n' && *v != '\r' && m < buflen - 1) buf[m++] = *v++;
while (m > 0 && (buf[m-1] == ' ' || buf[m-1] == '\t')) m--;
buf[m] = 0;
found = 1;
break;
}
fclose(f);
return found ? 0 : -1;
}
/* ---- settings.conf 写一个字符串键(best-effort):流式拷贝到 .tmp,命中键行改写为
* "key = value"(launcher 落盘格式),键缺失则末尾追加;rename 原子替换。保留其余注释/设置,
* 绝不 O_TRUNC 整文件(同 meow_ui_vol_set 思路)。失败静默返回 -1(读不改状态、不崩)。 */
static int str_setting_set(const char *key, const char *val)
{
if (!key || !key[0] || !val) return -1;
FILE *in = fopen(MEOW_STR_SETTINGS_FILE, "r");
char tmp[MEOW_STR_PATH_MAX];
int kl = (int)strlen(key);
snprintf(tmp, sizeof tmp, "%s.tmp", MEOW_STR_SETTINGS_FILE);
FILE *out = fopen(tmp, "w");
if (!out) { if (in) fclose(in); return -1; }
int found = 0;
char line[256];
while (in && fgets(line, sizeof line, in)) {
char *eq = strchr(line, '=');
char *k = line;
while (*k == ' ' || *k == '\t') k++;
int n = eq ? (int)(eq - k) : 0;
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--;
if (eq && n == kl && strncmp(k, key, (size_t)kl) == 0) {
fprintf(out, "%s = %s\n", key, val);
found = 1;
} else {
fputs(line, out);
}
}
if (in) fclose(in);
if (!found) fprintf(out, "%s = %s\n", key, val);
fclose(out);
if (rename(tmp, MEOW_STR_SETTINGS_FILE) != 0) { remove(tmp); return -1; }
return 0;
}
/* ---- TF 覆盖表路径:<MEOW_DATA_DIR 去尾 /data>/strings/<locale>.txt(= MeowData/<pkg>/strings/)。
* 无 MEOW_DATA_DIR(手动跑/宿主/无卡)→ path 置空(覆盖表不加载,内嵌兜底)。返回 path 是否有效。 */
static int str_override_path(char *path, int n)
{
const char *env = getenv("MEOW_DATA_DIR");
if (!env || !env[0] || n <= 0) { if (n > 0) path[0] = 0; return 0; }
char base[MEOW_STR_PATH_MAX];
snprintf(base, sizeof base, "%s", env);
size_t bn = strlen(base);
/* launcher 注入的 MEOW_DATA_DIR = <pkg>/data;去尾 "/data" 回到 <pkg>,再拼 strings/。 */
if (bn >= 5 && strcmp(base + bn - 5, "/data") == 0) base[bn - 5] = 0;
if (snprintf(path, (size_t)n, "%s/strings/%s.txt", base, g_locale) >= n) { path[0] = 0; return 0; }
return path[0] != 0;
}
/* 解析覆盖表(`id=text` 行,'=' 两侧去空白;空行/无 '=' 行跳过;UTF-8 原样透传)。
* 覆盖优先级:同名覆盖内嵌,未覆盖该 id 则回退内嵌。 */
static void str_load_override(void)
{
char path[MEOW_STR_PATH_MAX];
g_ov_n = 0;
if (!str_override_path(path, sizeof path)) return;
FILE *f = fopen(path, "r");
if (!f) return;
char line[MEOW_STR_OVR_TEXT_MAX + 16];
while (fgets(line, sizeof line, f) && g_ov_n < MEOW_STR_OVERRIDE_MAX) {
char *eq = strchr(line, '=');
if (!eq) continue; /* 注释/空行/非 key=value */
char *k = line;
while (*k == ' ' || *k == '\t') k++; /* id 左去空白 */
int klen = (int)(eq - k);
while (klen > 0 && (k[klen-1] == ' ' || k[klen-1] == '\t')) klen--;
if (klen <= 0 || klen >= MEOW_STR_OVR_ID_MAX) continue;
const char *v = eq + 1;
while (*v == ' ' || *v == '\t') v++; /* text 左去空白 */
int vlen = 0;
while (v[vlen] && v[vlen] != '\n' && v[vlen] != '\r' && vlen < MEOW_STR_OVR_TEXT_MAX - 1)
vlen++;
while (vlen > 0 && (v[vlen-1] == ' ' || v[vlen-1] == '\t')) vlen--; /* text 右去空白 */
if (vlen <= 0) continue;
memcpy(g_ov_id[g_ov_n], k, (size_t)klen);
g_ov_id[g_ov_n][klen] = 0;
memcpy(g_ov_text[g_ov_n], v, (size_t)vlen);
g_ov_text[g_ov_n][vlen] = 0;
g_ov_n++;
}
fclose(f);
}
/* 惰性初值:首次调用读 settings.conf ui.locale(缺省 "zh_cn")。幂等(只读一次)。 */
static void str_init_locale(void)
{
if (g_locale_loaded) return;
char buf[MEOW_STR_LOCALE_MAX];
if (str_setting_get("ui.locale", buf, sizeof buf) == 0 && buf[0])
snprintf(g_locale, sizeof g_locale, "%s", buf);
else
snprintf(g_locale, sizeof g_locale, "zh_cn");
g_locale_loaded = 1;
}
/* 惰性加载覆盖表(对当前 g_locale)。幂等(只加载一次直到 set_locale 重载)。 */
static void str_ensure_override(void)
{
if (g_override_loaded) return;
str_load_override();
g_override_loaded = 1;
}
/* meow_str / meow_str_locale 前置:确保 locale 初值 + 覆盖表就绪。 */
static void str_init(void)
{
str_init_locale();
str_ensure_override();
}
/* 持久化 ui.locale(best-effort)。失败静默(运行时仍切换,仅不跨启动)。 */
static void str_persist_locale(void)
{
str_setting_set("ui.locale", g_locale);
}
const char *meow_str_locale(void)
{
str_init();
return g_locale;
}
const char *meow_str(const char *id)
{
if (!id) return "";
str_init();
int en = (strcmp(g_locale, "en_us") == 0);
/* ① 当前 locale 的 TF 覆盖表(最高优先) */
for (int i = 0; i < g_ov_n; i++)
if (strcmp(g_ov_id[i], id) == 0) return g_ov_text[i];
/* ② 内嵌当前语言(en_us→en;其余→zh)③ 兜底内嵌内置默认(zh)——同一表内按语言取。 */
for (size_t i = 0; i < MEOW_STR_EMBED_N; i++)
if (strcmp(g_embed[i].id, id) == 0) return en ? g_embed[i].en : g_embed[i].zh;
/* ④ 未知 id:原样返回调用方(永不 NULL)。 */
return id;
}
int meow_strf(char *out, int outsz, const char *id, ...)
{
if (!out || outsz <= 0) return -1;
const char *s = meow_str(id);
va_list ap;
va_start(ap, id);
int r = vsnprintf(out, (size_t)outsz, s, ap);
va_end(ap);
return r;
}
void meow_str_set_locale(const char *locale)
{
if (!locale || !locale[0]) return;
str_init(); /* 确保 locale 初值 + 覆盖表基座就绪 */
snprintf(g_locale, sizeof g_locale, "%s", locale);
str_load_override(); /* 按新 locale 重载覆盖表 */
g_override_loaded = 1;
str_persist_locale(); /* best-effort 持久化(失败静默) */
}
+27
View File
@@ -37,6 +37,12 @@ static char g_background[64] = ""; /* display.background(black
static meow_theme_listener g_listeners[8];
static int g_nlisteners = 0;
/* §8.1.5 色板注入:一份自定义板覆写当前生效主题(src==NULL 恢复内建)。
* 覆写优先于 settings 解析(随 meow_theme_reload() 重读 settings 后仍生效,与 meow_ui_set_style
* 的「程序化覆写不清 settings」语义一致)。 */
static meow_theme g_inject;
static int g_inject_set = 0;
/* ---- 读 /usr/data/meow/settings.conf 的 key=value(兼容 "key = value" 带空格形式)----
* 与 launcher meow_settings_import 同款解析:按 '=' 切分,key/value 各自去首尾空白,
* 精确匹配 key(避免 display.theme 误匹配 display.theme_mode)。返回 0=命中,-1=未命中。 */
@@ -295,6 +301,7 @@ static void broadcast(void)
/* 当前「style×mode」主题:style=0/1(MD3/MIUIX),dark=0(light)/1(dark)。 */
static const meow_theme *theme_current(int style, int dark)
{
if (g_inject_set) return &g_inject; /* §8.1.5 注入板优先 */
if (style < MEOW_STYLE_MD3) style = MEOW_STYLE_MD3;
if (style > MEOW_STYLE_MIUIX) style = MEOW_STYLE_MIUIX;
return &g_theme[style][dark ? 1 : 0];
@@ -339,6 +346,26 @@ meow_theme meow_theme_defaults(meow_theme_mode m)
return g_theme[MEOW_STYLE_MD3][resolve_mode(m) == MEOW_THEME_DARK ? 1 : 0];
}
/* ---- §8.1.5 色板导出/注入(单一权威桥)----
* 导出:把当前「style×mode」解析生效的完整权威主题拷给调用方(不取指针以防调用方误改内部表)。
* 注入:用一份自定义板覆写当前生效主题;NULL 恢复内建。二者都触发 load(g_loaded==0 时先读
* settings.conf),使宿主无 settings 文件(单测/脱机)也可靠。 */
void meow_theme_export(meow_theme *out)
{
if (!out) return;
const meow_theme *t = meow_theme_get(); /* 解析 g_mode(AUTO→明/暗)+ g_style + 背景覆写 */
*out = *t;
}
void meow_theme_inject(const meow_theme *src)
{
if (!g_loaded) { load_settings(); g_loaded = 1; }
if (!src) { g_inject_set = 0; broadcast(); return; }
g_inject = *src;
g_inject_set = 1;
broadcast(); /* 通知 App/状态栏/控件下一帧用新注入板重绘 */
}
/* ---- §8.1.2 顶级主题风格族(minimal)----
* meow_ui_style() 返回当前 ui.style(读取走 load_settings,随 meow_theme_reload() 重读,
* 故 launcher 改 settings.conf 后经 sb_sdk_refresh→theme_reload 即跟手)。
+147
View File
@@ -0,0 +1,147 @@
/* meow_ui_time.c — SDK 单调时钟(meow_ui_time_ms)+ mipsel o32 时间 ABI 兜底。
*
* 根因(M11;真机日志 + 反汇编证据链见 sdk/CLOCK_ABI.md):
* zig 0.17 的 clang 前端给 32 位 mips 目标预定义了 __ILP32__;zig 自带 musl 的
* time64 回退分支统一写作 `#if defined(SYS_x_time64) && !defined(__ILP32__)`
* (该 `!__ILP32__` 本意只排除 x32),于是 mipsel 的 libc 把「time64 入口」编译成了
* 老 32 位实现:__clock_gettime64 直接做 SYS_clock_gettime(4263),只写 8 字节
* `{ int32 tv_sec; int32 tv_nsec; }`。而 zig 头文件又无条件把 time_t 声明成 64 位
* (struct timespec 16 字节、tv_sec 8 字节),两边 ABI 不一致:
* tv_sec 低字 = 真 sec,高字 = 真 nsec → 64 位结果高 32 位随机、非单调;
* tv_nsec(结构偏移 8)根本没被写 → 毫秒位恒 0(真机实测全是 1000 的倍数)。
* 容器旧链(gcc)不定义 __ILP32__,time64 回退正确——对照 patch_v2/ncm_app/ncm:
* 同一函数里先 SYS_clock_gettime64(4403)、ENOSYS 后走 4263 并做宽度转换。
*
* 修复:不依赖 libc 头声明的 struct timespec 宽度,直接按**内核 o32 ABI**
* (8 字节 time32:int32 sec + int32 nsec)取时,再自行升成 64 位毫秒;
* 同时在本 TU 给出 __clock_gettime / __clock_gettime64 的强定义。应用链接顺序是
* `... meow_ui.a ... -lc`,归档先参与解析,因此链接期整体替换掉 libc 里那份
* ABI 不一致的实现:time()、gettimeofday()、pthread 定时等待的内部取时
* (musl 的 time()/__timedwait_cp 都调 __clock_gettime)随之修正。
* 上层时间位宽仍是 64 位,只有内核取数按 o32 time32;2038 前有效。
*
* 同一句 `!__ILP32__` 还打穿了睡眠:__nanosleep_time64/__clock_nanosleep 走 `#else`
* 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
* `usleep(16000)` 立即返回(SDK 里帧节拍/轮询会变忙等空转)。这里一并按 time32
* 转换后重发,恢复亚秒睡眠。
*
* 残余(未修):futex 相对超时(pthread 定时等待的剩余时间按 8 字节读)、timerfd_settime、
* clock_settime/adjtime、pselect/ppoll 超时——同为该守卫的受害者,见 CLOCK_ABI.md §6。
*/
#include <errno.h>
#include <stdint.h>
#include <time.h>
#include "meow_ui.h"
#if defined(__mips__) && !defined(__mips64) && (__SIZEOF_LONG__ == 4)
#define MEOW_TIME32_KERNEL_ABI 1
#include <sys/syscall.h>
#include <unistd.h>
#else
#define MEOW_TIME32_KERNEL_ABI 0
#endif
uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec)
{
return (uint64_t)sec * 1000u + (uint64_t)nsec / 1000000u;
}
uint64_t meow_ui_time_ms(void)
{
#if MEOW_TIME32_KERNEL_ABI
struct meow_time32 { int32_t tv_sec; int32_t tv_nsec; } t;
t.tv_sec = 0;
t.tv_nsec = 0;
if (syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &t) != 0) return 0;
return meow_time_ms_from_parts(t.tv_sec, t.tv_nsec);
#else
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
return meow_time_ms_from_parts((int64_t)ts.tv_sec, (int64_t)ts.tv_nsec);
#endif
}
#if MEOW_TIME32_KERNEL_ABI
/* 内核 o32 time32 布局(与 libc 头的 time_t 宽度无关)。 */
struct meow_time32 {
int32_t tv_sec;
int32_t tv_nsec;
};
static int32_t time32_clamp(int64_t v)
{
if (v > INT32_MAX) return INT32_MAX;
if (v < 0) return 0;
return (int32_t)v;
}
/* ---- libc 取时入口的强定义 ----
* 返回约定与 musl 一致:0 成功 / -1 失败(errno)。
* 说明:<time.h> 在 _REDIR_TIME64 下把标识符 clock_gettime 的引用改名为
* __clock_gettime64,故本 TU 不定义 clock_gettime 这个名字;musl 里它只是
* __clock_gettime 的 weak alias,同一份语义已由下面的强定义接管。 */
int __clock_gettime(clockid_t clk, struct timespec *ts);
int __clock_gettime64(clockid_t clk, struct timespec *ts);
int __clock_gettime(clockid_t clk, struct timespec *ts)
{
struct meow_time32 t;
if (!ts) { errno = EFAULT; return -1; }
t.tv_sec = 0;
t.tv_nsec = 0;
if (syscall(SYS_clock_gettime32, clk, &t) != 0) return -1;
ts->tv_sec = t.tv_sec;
ts->tv_nsec = t.tv_nsec;
return 0;
}
int __clock_gettime64(clockid_t clk, struct timespec *ts)
{
return __clock_gettime(clk, ts);
}
/* ---- 睡眠入口(同源缺陷:亚秒 usleep 变空转)----
* __clock_nanosleep 按 musl 内部约定返回 0 或正 errno;__nanosleep_time64 是
* nanosleep/usleep 的重定向目标,按公共约定返回 0/-1(errno)。 */
int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem);
int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem);
int __nanosleep_time64(const struct timespec *req, struct timespec *rem);
int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem)
{
struct meow_time32 rq, rm;
long r;
if (clk == CLOCK_THREAD_CPUTIME_ID) return EINVAL;
if (!req) return EFAULT;
if (req->tv_nsec < 0 || req->tv_nsec >= 1000000000L) return EINVAL;
rq.tv_sec = time32_clamp(req->tv_sec);
rq.tv_nsec = (int32_t)req->tv_nsec;
rm.tv_sec = 0;
rm.tv_nsec = 0;
if (clk == CLOCK_REALTIME && !flags)
r = syscall(SYS_nanosleep, &rq, rem ? &rm : 0);
else
r = syscall(SYS_clock_nanosleep_time32, clk, flags, &rq, rem ? &rm : 0);
if (r != 0) return errno;
if (rem) {
rem->tv_sec = rm.tv_sec;
rem->tv_nsec = rm.tv_nsec;
}
return 0;
}
int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem)
{
return __clock_nanosleep(clk, flags, req, rem);
}
int __nanosleep_time64(const struct timespec *req, struct timespec *rem)
{
int r = __clock_nanosleep(CLOCK_REALTIME, 0, req, rem);
if (r) { errno = r; return -1; }
return 0;
}
#endif
+58 -2
View File
@@ -9,6 +9,7 @@
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "meow_ui_view.h"
#include "include/meow_ui_box.h"
#include <stdlib.h>
#include <string.h>
@@ -17,6 +18,50 @@
static meow_view *g_view_by_id[MEOW_VIEW_BY_ID_MAX]; /* id → 顶层页 view(page_push/pop 查找) */
static meow_view *g_top_view = NULL; /* 当前进入的顶层页 view(生命周期跟踪) */
/* ---- 布局提供者注册表(view → meow_box,opt-in) ---- */
static struct { meow_view *v; meow_box *box; } g_view_layout[MEOW_VIEW_BY_ID_MAX];
static int g_view_layout_n = 0;
static void layout_reg_set(meow_view *v, meow_box *box)
{
for (int i = 0; i < g_view_layout_n; i++) {
if (g_view_layout[i].v == v) {
g_view_layout[i].box = box;
return;
}
}
if (g_view_layout_n < MEOW_VIEW_BY_ID_MAX) {
g_view_layout[g_view_layout_n].v = v;
g_view_layout[g_view_layout_n].box = box;
g_view_layout_n++;
}
}
static meow_box *layout_reg_get(const meow_view *v)
{
for (int i = 0; i < g_view_layout_n; i++)
if (g_view_layout[i].v == v) return g_view_layout[i].box;
return NULL;
}
/* 重排:把 view 的布局 box 解算一次(box 矩形未设时自动跟 view 内容矩形)。 */
void meow_view_relayout(meow_view *v)
{
if (!v) return;
meow_box *b = layout_reg_get(v);
if (!b) return;
int r[4]; meow_box_rect(b, r);
if (r[2] <= 0 || r[3] <= 0) /* w/h 未设 → 跟 view 内容矩形 */
meow_box_set_rect(b, v->x, v->y, v->w, v->h);
meow_box_arrange(b);
}
void meow_view_set_layout(meow_view *v, struct meow_box *box)
{
if (!v) return;
if (!box) { layout_reg_set(v, NULL); return; }
layout_reg_set(v, box);
meow_view_relayout(v); /* 立即按当前尺寸排一次 */
}
static void run_fn(meow_view_fn f, meow_view *v)
{
if (f) f(v, v->ctx);
@@ -111,6 +156,10 @@ int meow_view_add_child(meow_view *parent, meow_view *child)
if (parent->nchildren >= MEOW_VIEW_MAX_CHILD) return -1;
parent->children[parent->nchildren++] = child;
child->parent = parent;
/* 递归修正(§A.4「子视图随父进出」):父已进入时挂上的子视图**立即**进入
* (on_enter 不遗漏、参与事件分发)。页在 push/pop 时仍由 view_enter/view_leave 递归驱动,
* 此处只补「运行期动态挂子」的间隙(hello 目录嵌套实为 create 后 page_push 前 add,不受影响)。 */
if (parent->state & MEOW_VIEW_STATE_ENTERED) view_enter(child);
return 0;
}
@@ -146,6 +195,7 @@ void meow_view_page_push(int id)
if (cur && cur != next) view_leave(cur);
meow_ui_activity_push(id);
view_enter(next);
meow_view_relayout(next); /* 布局提供者(meow_box)随页进入自动排一次 */
g_top_view = next ? next : cur;
}
@@ -156,6 +206,7 @@ void meow_view_page_pop(void)
meow_ui_activity_pop();
meow_view *next = view_map_get(meow_ui_activity_active());
view_enter(next);
meow_view_relayout(next);
g_top_view = next ? next : NULL;
}
@@ -167,14 +218,19 @@ void meow_view_page_replace(int id)
meow_view *next = view_map_get(id);
if (cur && cur != next) view_leave(cur);
view_enter(next);
meow_view_relayout(next);
g_top_view = next ? next : cur;
}
/* ---- 页内子导航(预留扩展位 a 桩:本批不实现,仅返回占位)---- */
/* ---- 页内子导航(预留扩展位 a,**标注废弃**)----
* 设计定案 b(SDK_VIEW_FRAMEWORK_DESIGN.md §F.2:只做子视图容器嵌套,顶层页切仍走 activity
* 栈,不做页内子导航栈):本函数对为「将来 a」预留的桩,**不实现、不供依赖**——调用方不得
* 认为会建立/弹出子导航。页内「子页」请用 meow_view_add_child 挂 CONTAINER 子视图(组合嵌套),
* 或以 activity 栈驱动页切换(page_push/pop/replace)。保留符号仅为未来 a 的占位。 */
int meow_view_sub_push(meow_view *v, meow_view *sub)
{
(void)v; (void)sub;
return 0; /* 预留:将来在 ACTIVITY view 内维护子栈 */
return 0; /* 预留:将来在 ACTIVITY view 内维护子栈(见设计定案 b,不实现) */
}
meow_view *meow_view_sub_pop(meow_view *v)
{
+17 -4
View File
@@ -82,12 +82,17 @@ void meow_view_page_replace(int id); /* 旧top on_leave + 新页 on_en
/* 入口:把语义事件注入当前活动页 view 子树路由——先发子视图(按 z 降序),命中即消费,
* 再落本 view 的 on_event(其内做该 view 的 widget 命中)。返回 1=已消费(§A.7)。
* 未接线于 meow_ui_run(App 可在 on_tap/on_swipe 等回调里改用它实现 SDK 原生路由)。 */
* 已接线于 meow_ui_run(M7.0a):在 §6.4 分流之上,TAP/KEY/SWIPE/LONG_PRESS 先经本路由,
* 命中即消费(不再落 App 的 on_* 回调);返回 0 时回落 on_* 原路径(无 view 树 / 未命中 →
* 完全向后兼容)。App 也可在自持事件循环里直接调用本函数实现 SDK 原生路由。 */
int meow_view_dispatch_event(int ev, int a, int b);
/* ---- 页内子导航(预留扩展位 a;本批为空实现/桩,仅在 ACTIVITY view 内维护子栈)---- */
int meow_view_sub_push(meow_view *v, meow_view *sub); /* 预留;返回 0(未实现于本批) */
meow_view *meow_view_sub_pop(meow_view *v); /* 预留;返回 NULL(未实现于本批) */
/* ---- 页内子导航(预留扩展位 a,**标注废弃**)----
* 设计定案 b(§F.2):不做页内子导航栈,只做子视图容器嵌套;顶层页切仍走 activity 栈。
* 故本组为**不实现、不供依赖**的桩(返回 0/NULL);页内子页请用 meow_view_add_child 挂
* CONTAINER 子视图,或以 page_push/pop/replace 走 activity 栈。保留符号仅作将来 a 的占位。 */
int meow_view_sub_push(meow_view *v, meow_view *sub); /* 废弃(不实现);返回 0 */
meow_view *meow_view_sub_pop(meow_view *v); /* 废弃(不实现);返回 NULL */
/* ---- 坐标:view 相对父内容原点的累计(父坐标链求和 → 屏幕绝对坐标)---- */
int meow_view_screen_x(const meow_view *v);
@@ -97,6 +102,14 @@ int meow_view_screen_y(const meow_view *v);
meow_view *meow_view_parent(const meow_view *v);
int meow_view_get_state(const meow_view *v); /* MEOW_VIEW_STATE_* 位掩码 */
/* ---- 布局提供者(§12.1.4:meow_box 作为 view 的 layout,opt-in)----
* meow_box(见 include/meow_ui_box.h)是**纯布局计算器**,arrange 把子控件算成绝对坐标。
* 设了布局的 view:其直接挂载控件应**同时**加入该 box(box 会在 arrange 时写这些控件的
* x/y/w/h);page 进入/relayout 时框架自动 arrange。未设布局的 view 保持现有绝对坐标行为
* (on_layout 里自己算,零改动)。box 矩形默认跟 view 内容矩形(w/h 未设时自动填)。 */
void meow_view_set_layout(meow_view *v, struct meow_box *box); /* NULL=取消 */
void meow_view_relayout(meow_view *v); /* 手动重排(改 box 后调) */
#ifdef __cplusplus
}
#endif
+795 -109
View File
File diff suppressed because it is too large. Load diff
+557 -6
View File
@@ -1,6 +1,11 @@
/* sb_icons.h — 状态栏/通知中心图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。
* 生成自 material-symbols@0.47.0 rounded 字体光栅化;每像素 0..255(亮度/alpha)。
* ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。 */
/* sb_icons.h — 状态栏 + 动作位图图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。
* 生成自 material-symbols rounded(google/material-design-icons,Apache-2.0)SVG 栅格化:
* 图标族 = FILL=1 实心(与状态栏 WiFi/BT/BATT 现有实心风格一致);每像素 0..255(亮度/alpha)。
* ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。
* 索引布局(兼容):状态栏 SB_I_WIFI=0..SB_I_BATT_CHG=14(原样保留);TopAppBar 动作
* MEOW_ICON_SETTINGS=15..MEOW_ICON_HEART=29(只追加,既有索引不变)。动作图标为 15-29,
* 经 meow_topappbar_set_item_icon(w, idx, MEOW_ICON_*) 使 W_TOPAPPBAR 动作项画位图。
* 重新生成:python3 tools/gen_action_icons.py(SVG 缓存 tools/icons_src/,可复现)。 */
#ifndef SB_ICONS_H
#define SB_ICONS_H
@@ -196,6 +201,521 @@ static const unsigned char SB_ICON_BATT_CHG[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,191,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,63,255,47,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,207,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,239,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,255,255,223,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,63,255,255,255,255,207,63,63,63,63,63,63,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,191,255,255,255,255,255,255,255,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,255,255,255,255,255,255,255,255,255,255,239,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,223,255,255,255,255,255,255,255,255,255,255,111,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,255,255,255,255,255,255,255,255,255,255,191,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,239,255,255,255,255,255,255,255,255,255,239,31,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,111,255,255,255,255,79,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,175,255,255,255,175,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,239,255,255,239,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,47,255,255,255,79,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,255,255,175,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,255,239,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,223,255,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,31,255,143,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,79,223,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,143,63,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,95,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
};
/* ===== TopAppBar 动作图标(Material Symbols Rounded, MD3, Apache-2.0)====
* 复用状态栏同源 32x32 单通道 alpha;绘制颜色/按压态由 meow_ui_widget.c 主题表提供。 */
static const unsigned char SB_ICON_SETTINGS[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,1,0,40,84,85,85,84,40,0,1,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,3,0,83,248,255,255,255,255,248,83,0,3,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,2,3,1,0,1,2,213,255,249,252,252,249,255,213,2,1,0,1,3,2,0,0,0,0,0,0,
0,0,0,0,0,2,0,0,0,5,3,17,239,254,254,255,255,254,254,239,17,3,5,0,0,0,2,0,0,0,0,0,
0,0,0,0,2,0,33,55,5,0,0,45,254,254,255,255,255,255,254,254,46,0,0,5,55,33,0,2,0,0,0,0,
0,0,0,3,0,104,246,255,216,115,95,221,254,254,255,255,255,255,254,254,223,96,114,216,255,246,104,0,3,0,0,0,
0,0,2,1,36,251,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,255,251,36,1,2,0,0,
0,0,3,0,174,255,251,255,253,252,252,253,255,254,251,251,251,251,254,255,253,252,252,253,255,251,255,174,0,3,0,0,
0,3,0,67,255,252,255,255,255,255,255,255,253,255,255,255,255,255,255,253,255,255,255,255,255,255,252,255,67,0,3,0,
0,3,0,171,253,251,255,255,255,255,255,253,255,243,151,95,92,142,234,255,253,255,255,255,255,255,251,253,171,0,3,0,
0,4,0,149,255,248,255,255,255,255,253,255,216,41,0,0,0,0,28,198,255,253,255,255,255,255,248,255,149,0,4,0,
0,2,1,29,217,255,254,255,255,254,254,245,41,0,6,4,4,5,0,20,231,254,254,255,255,254,255,217,29,1,2,0,
0,0,1,0,19,172,253,253,255,251,255,163,0,5,0,0,0,0,5,0,129,255,251,255,253,253,172,19,0,1,0,0,
0,0,0,3,0,11,238,255,254,251,255,107,1,4,0,0,0,0,3,1,75,255,252,254,255,239,11,0,3,0,0,0,
0,0,0,3,0,10,238,255,254,251,255,107,1,4,0,0,0,0,3,1,75,255,252,254,255,237,9,0,2,0,0,0,
0,0,1,0,19,172,253,253,255,251,255,163,0,5,0,0,0,0,6,0,129,255,251,255,253,252,167,16,0,1,0,0,
0,2,1,29,217,255,254,255,255,254,254,245,41,0,6,4,4,5,0,21,231,254,254,255,255,254,255,212,25,1,1,0,
0,4,0,149,255,248,255,255,255,255,253,255,217,41,0,0,0,0,28,198,255,253,255,255,255,255,248,255,140,0,4,0,
0,3,0,171,253,251,255,255,255,255,255,253,255,243,152,96,93,143,234,255,253,255,255,255,255,255,251,253,162,0,4,0,
0,3,0,67,255,252,255,255,255,255,255,255,253,255,255,255,255,255,255,253,255,255,255,255,255,255,252,254,60,0,3,0,
0,0,3,0,174,255,251,255,253,252,252,253,255,254,251,251,251,251,254,255,253,252,252,253,255,251,255,162,0,3,0,0,
0,0,2,1,36,251,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,255,244,26,2,2,0,0,
0,0,0,3,0,104,246,255,216,115,96,223,254,254,255,255,255,255,254,254,222,95,117,220,255,241,90,0,3,0,0,0,
0,0,0,0,2,0,33,55,5,0,0,46,254,254,255,255,255,255,254,254,45,0,0,8,56,29,0,2,0,0,0,0,
0,0,0,0,0,2,0,0,0,5,3,17,239,254,254,255,255,254,254,239,17,3,5,0,0,0,2,0,0,0,0,0,
0,0,0,0,0,0,2,3,1,0,1,2,213,255,249,252,252,249,255,213,2,1,0,1,3,2,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,3,0,83,248,255,255,255,255,248,83,0,3,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,1,0,40,84,85,85,84,40,0,1,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_REFRESH[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,3,3,4,4,2,0,0,0,0,3,4,2,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,3,2,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,3,0,0,40,100,144,164,164,141,94,32,0,0,5,0,88,159,39,0,2,0,0,0,
0,0,0,0,0,0,0,1,2,0,58,180,247,255,255,255,255,255,255,242,167,47,0,15,238,255,163,0,4,0,0,0,
0,0,0,0,0,0,1,2,0,127,253,255,255,255,251,237,237,252,255,255,255,248,109,4,233,251,169,0,3,0,0,0,
0,0,0,0,0,0,3,0,153,255,254,255,192,98,43,21,21,45,102,197,255,255,255,151,225,255,168,0,3,0,0,0,
0,0,0,0,0,3,0,127,255,251,245,97,0,0,0,0,0,0,0,0,101,246,254,255,251,255,169,0,3,0,0,0,
0,0,0,0,3,0,53,255,252,245,63,0,3,4,2,1,1,4,1,0,0,58,242,251,250,255,169,0,3,0,0,0,
0,0,0,0,2,0,184,255,255,95,0,6,0,0,0,0,0,0,12,18,23,8,116,255,249,255,168,0,3,0,0,0,
0,0,0,2,0,37,250,254,195,0,4,0,0,0,0,3,0,89,226,232,234,236,229,251,248,251,170,0,3,0,0,0,
0,0,0,4,0,102,255,255,95,1,4,0,0,0,0,4,0,172,255,255,255,255,255,255,255,255,165,0,4,0,0,0,
0,0,0,4,0,146,255,250,40,0,2,0,0,0,0,2,0,39,158,168,168,168,168,169,169,159,41,0,2,0,0,0,
0,0,0,3,0,165,255,236,20,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,3,0,165,255,236,20,0,1,0,0,0,0,0,0,2,4,3,3,3,3,4,4,4,2,0,0,0,0,0,
0,0,0,4,0,146,255,250,40,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0,
0,0,0,4,0,102,255,255,95,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,0,
0,0,0,2,0,37,250,254,195,0,4,0,0,0,0,0,0,0,0,0,0,4,0,160,226,88,0,3,0,0,0,0,
0,0,0,0,2,0,184,255,255,95,0,6,0,0,0,0,0,0,0,0,6,0,100,255,255,165,0,4,0,0,0,0,
0,0,0,0,3,0,53,255,252,245,63,0,3,4,2,1,1,2,4,2,0,65,246,251,249,44,0,2,0,0,0,0,
0,0,0,0,0,3,0,127,255,251,245,97,0,0,0,0,0,0,0,0,102,245,251,255,115,0,3,0,0,0,0,0,
0,0,0,0,0,0,3,0,153,255,254,255,192,98,43,21,22,44,102,197,255,255,255,143,0,3,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,0,127,253,255,255,255,251,237,237,251,255,255,255,248,116,0,3,1,0,0,0,0,0,0,
0,0,0,0,0,0,0,1,2,0,57,180,247,255,255,255,255,255,255,244,171,49,0,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,3,0,0,40,100,144,164,164,142,97,35,0,0,3,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,3,3,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_SEARCH[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,1,4,2,0,0,0,1,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,3,0,0,0,7,17,11,0,0,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,3,0,19,111,185,221,233,227,201,141,48,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,3,0,88,228,255,255,255,255,255,255,255,250,143,5,0,1,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,3,0,117,255,255,255,241,194,171,182,224,255,255,255,185,8,2,1,0,0,0,0,0,0,0,0,0,0,
0,0,0,3,0,85,255,250,255,142,30,0,0,0,12,95,232,253,255,165,0,3,0,0,0,0,0,0,0,0,0,0,
0,0,1,1,18,234,253,254,97,0,0,2,3,2,0,0,37,227,252,255,79,0,3,0,0,0,0,0,0,0,0,0,
0,0,4,0,112,255,255,144,0,6,2,0,0,0,1,6,0,61,252,254,195,0,2,0,0,0,0,0,0,0,0,0,
0,0,2,0,189,254,241,23,2,2,0,0,0,0,0,0,4,0,182,255,247,30,0,2,0,0,0,0,0,0,0,0,
0,1,0,7,223,255,192,0,2,0,0,0,0,0,0,0,4,1,111,255,255,69,0,3,0,0,0,0,0,0,0,0,
0,1,0,17,233,255,168,1,3,0,0,0,0,0,0,0,4,0,86,255,254,85,0,4,0,0,0,0,0,0,0,0,
0,1,0,11,228,255,180,0,2,0,0,0,0,0,0,0,4,1,98,255,255,76,0,3,0,0,0,0,0,0,0,0,
0,0,1,0,204,255,225,7,2,1,0,0,0,0,0,0,4,0,153,255,251,40,0,2,0,0,0,0,0,0,0,0,
0,0,4,0,144,255,255,93,0,6,0,0,0,0,0,3,0,21,236,255,209,3,1,0,0,0,0,0,0,0,0,0,
0,0,2,0,44,254,253,233,36,0,4,4,4,4,4,0,0,178,255,255,101,0,4,0,0,0,0,0,0,0,0,0,
0,0,0,3,0,143,255,253,225,66,0,0,0,0,0,27,175,255,254,207,0,4,1,0,0,0,0,0,0,0,0,0,
0,0,0,1,3,5,185,255,255,254,181,115,90,102,154,235,255,255,253,242,99,0,5,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,1,8,163,255,255,255,255,255,255,255,255,255,207,252,255,255,100,0,5,0,0,0,0,0,0,0,0,
0,0,0,0,0,1,0,0,82,191,243,255,255,255,249,204,104,2,98,255,251,255,100,0,5,0,0,0,0,0,0,0,
0,0,0,0,0,0,1,2,0,0,31,67,83,74,41,0,0,2,0,99,255,251,255,100,0,5,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,3,1,0,0,0,0,0,0,3,1,5,0,99,255,251,255,100,0,5,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,2,3,4,3,2,0,0,0,0,5,0,99,255,251,255,100,0,3,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,99,255,251,255,100,0,2,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,99,255,251,254,69,0,3,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5,0,99,255,255,94,0,4,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,69,93,2,1,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_PLAY_ARROW[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,1,0,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,1,0,39,39,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,2,0,48,247,249,134,10,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,90,255,252,255,212,72,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,85,254,252,251,255,255,156,23,0,2,2,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,253,253,255,228,93,0,0,4,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,251,255,255,177,38,0,1,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,254,252,255,241,115,2,0,1,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,252,255,255,197,51,0,2,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,255,254,251,255,246,29,0,2,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,255,254,251,255,246,29,0,2,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,255,255,252,255,255,197,51,0,2,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,255,254,252,255,241,115,2,0,1,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,255,255,251,255,255,177,38,0,1,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,86,255,252,255,253,253,255,228,93,0,0,4,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,85,254,252,251,255,255,156,23,0,2,2,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,90,255,252,255,212,72,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,2,0,47,247,249,134,10,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,1,0,38,39,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,1,0,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_PAUSE[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,1,0,27,81,52,0,1,0,0,0,0,1,0,52,81,27,0,1,0,0,0,0,0,0,0,
0,0,0,0,0,0,3,0,70,238,255,255,139,0,2,0,0,2,0,139,255,255,238,70,0,3,0,0,0,0,0,0,
0,0,0,0,0,1,1,4,216,255,249,253,255,52,0,2,2,0,52,255,253,249,255,216,4,1,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,239,254,254,251,253,88,0,4,4,0,88,253,251,254,254,239,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,237,255,254,252,255,86,0,4,4,0,85,255,252,254,255,237,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,238,255,254,252,255,86,0,4,4,0,86,255,252,254,255,238,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,237,255,254,252,255,86,0,4,4,0,85,255,252,254,255,237,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,0,17,239,254,254,251,253,88,0,4,4,0,88,253,251,254,254,239,17,0,1,0,0,0,0,0,
0,0,0,0,0,1,1,4,216,255,249,253,255,52,0,2,2,0,52,255,253,249,255,216,4,1,1,0,0,0,0,0,
0,0,0,0,0,0,3,0,70,238,255,255,139,0,2,0,0,2,0,139,255,255,238,70,0,3,0,0,0,0,0,0,
0,0,0,0,0,0,0,1,0,27,80,52,0,1,0,0,0,0,1,0,52,80,27,0,1,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,2,3,2,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_SKIP_NEXT[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,1,3,1,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,2,2,156,230,86,0,3,0,0,0,0,0,
0,0,0,0,0,2,0,62,198,141,11,0,4,1,0,0,0,0,0,1,0,16,243,255,175,0,3,0,0,0,0,0,
0,0,0,0,0,4,0,173,255,255,213,66,0,0,3,0,0,0,0,1,0,17,232,254,167,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,252,247,255,255,141,11,0,4,1,0,0,1,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,253,253,255,212,65,0,0,3,0,1,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,255,255,251,255,255,140,10,0,2,1,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,255,255,255,253,254,255,210,61,0,3,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,255,255,255,255,255,251,255,252,45,0,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,255,255,255,255,255,251,255,253,45,0,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,255,255,255,253,254,255,210,61,0,3,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,255,255,251,255,255,140,10,0,2,1,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,169,255,251,253,253,255,212,65,0,0,3,0,1,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,252,247,255,255,142,11,0,4,1,0,0,1,0,17,234,255,168,0,3,0,0,0,0,0,
0,0,0,0,0,4,0,173,255,255,213,66,0,0,3,0,0,0,0,1,0,17,232,254,167,0,3,0,0,0,0,0,
0,0,0,0,0,2,0,62,198,141,11,0,4,1,0,0,0,0,0,1,0,16,243,255,175,0,3,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,2,2,155,230,86,0,3,0,0,0,0,0,
0,0,0,0,0,0,0,2,1,3,1,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_SKIP_PREV[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,1,3,1,2,0,0,0,0,0,0,0,
0,0,0,0,0,3,0,86,230,155,2,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,3,0,175,255,243,16,0,1,0,0,0,0,0,1,4,0,11,141,198,62,0,2,0,0,0,0,0,
0,0,0,0,0,3,0,167,254,232,17,0,1,0,0,0,0,3,0,0,66,213,255,255,173,0,4,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,1,0,0,1,4,0,11,141,255,255,247,252,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,1,0,3,0,0,64,211,255,253,253,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,1,2,0,10,139,255,255,251,255,255,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,3,0,60,210,255,254,253,255,255,255,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,0,46,253,255,251,255,255,255,255,255,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,0,45,253,255,251,255,255,255,255,255,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,3,0,60,210,255,254,253,255,255,255,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,1,2,0,10,139,255,255,251,255,255,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,1,0,3,0,0,64,211,255,253,253,251,255,169,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,168,255,234,17,0,1,0,0,1,4,0,10,141,255,255,247,252,168,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,167,254,232,17,0,1,0,0,0,0,3,0,0,66,213,255,255,173,0,4,0,0,0,0,0,
0,0,0,0,0,3,0,175,255,243,16,0,1,0,0,0,0,0,1,4,0,11,141,198,62,0,2,0,0,0,0,0,
0,0,0,0,0,3,0,86,230,155,2,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,1,3,1,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_ARROW_BACK[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,18,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,60,232,228,41,1,2,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,4,0,60,242,255,255,73,0,3,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,147,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,4,0,60,242,255,255,152,0,7,5,3,4,4,4,4,4,4,4,4,3,1,0,0,0,0,0,
0,0,0,0,0,3,0,60,242,255,250,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,0,59,242,255,253,213,81,88,87,85,86,86,86,86,86,86,86,86,85,86,76,5,0,1,0,0,0,
0,0,0,1,1,20,237,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,139,0,4,0,0,0,
0,0,0,1,1,20,237,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,139,0,4,0,0,0,
0,0,0,0,2,0,59,242,255,253,213,81,88,87,85,86,86,86,86,86,86,86,86,85,86,76,5,0,1,0,0,0,
0,0,0,0,0,3,0,60,242,255,250,133,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,4,0,60,242,255,255,153,0,7,5,3,4,4,4,4,4,4,4,4,3,1,0,0,0,0,0,
0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,3,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,149,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,4,0,60,242,252,255,148,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,4,0,60,242,255,255,77,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,60,233,229,42,1,2,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,20,18,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_CLOSE[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,3,2,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,2,0,72,184,90,0,4,0,0,0,0,0,0,0,0,4,0,90,184,72,0,2,0,0,0,0,0,
0,0,0,0,0,3,0,202,255,255,100,0,5,0,0,0,0,0,0,5,0,99,255,255,202,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,90,254,251,255,100,0,5,0,0,0,0,5,0,99,255,251,254,90,0,3,0,0,0,0,0,
0,0,0,0,0,0,2,0,99,255,251,255,100,0,5,0,0,5,0,99,255,251,255,99,0,2,0,0,0,0,0,0,
0,0,0,0,0,0,0,4,0,99,255,251,255,100,0,5,5,0,99,255,251,255,99,0,4,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,5,0,99,255,251,255,100,0,0,100,255,251,255,99,0,5,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,5,0,99,255,251,255,90,90,255,251,255,99,0,5,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,5,0,99,255,254,255,255,254,255,100,0,5,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,5,0,89,249,252,252,249,90,0,5,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,5,0,90,249,252,252,249,90,0,5,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,5,0,100,255,254,255,255,254,255,100,0,5,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,5,0,99,255,251,255,90,89,255,251,255,100,0,5,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,5,0,99,255,251,255,100,0,0,99,255,251,255,100,0,5,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,4,0,99,255,251,255,99,0,5,5,0,99,255,251,255,100,0,4,0,0,0,0,0,0,0,
0,0,0,0,0,0,2,0,99,255,251,255,99,0,5,0,0,5,0,99,255,251,255,100,0,2,0,0,0,0,0,0,
0,0,0,0,0,3,0,90,254,251,255,99,0,5,0,0,0,0,5,0,99,255,251,254,91,0,3,0,0,0,0,0,
0,0,0,0,0,3,0,202,255,255,99,0,5,0,0,0,0,0,0,5,0,99,255,255,201,0,3,0,0,0,0,0,
0,0,0,0,0,2,0,71,184,90,0,4,0,0,0,0,0,0,0,0,4,0,90,184,71,0,2,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,3,2,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_ADD[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,60,60,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,3,0,56,255,255,56,0,3,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,90,254,254,90,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,85,255,255,85,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,3,4,4,4,4,8,4,88,255,255,88,4,8,4,4,4,4,3,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,255,255,75,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,1,0,55,86,85,86,86,88,85,143,255,255,143,85,88,86,86,85,86,55,0,1,0,0,0,0,0,
0,0,0,0,3,0,60,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,60,0,3,0,0,0,0,
0,0,0,0,3,0,60,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,60,0,3,0,0,0,0,
0,0,0,0,0,1,0,55,86,85,86,86,88,85,143,255,255,143,85,88,86,86,85,86,55,0,1,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,75,255,255,75,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,3,4,4,4,4,8,4,88,255,255,88,4,8,4,4,4,4,3,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,85,255,255,85,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,90,254,254,90,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,3,0,56,255,255,56,0,3,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,60,60,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_CHECK[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,3,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,99,91,0,1,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,141,255,255,62,0,3,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,142,255,252,242,41,1,2,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,142,255,252,246,66,0,2,0,0,0,0,
0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,3,0,142,255,252,246,67,0,3,0,0,0,0,0,
0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,1,3,0,142,255,252,246,68,0,4,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,0,2,0,0,0,0,0,1,3,0,142,255,251,246,68,0,4,0,0,0,0,0,0,0,
0,0,0,1,1,23,197,212,42,0,4,0,0,0,1,3,0,142,255,251,247,69,0,4,0,0,0,0,0,0,0,0,
0,0,0,3,0,76,255,255,230,42,0,4,0,1,3,0,142,255,251,247,69,0,4,0,0,0,0,0,0,0,0,0,
0,0,0,1,2,11,179,255,254,229,41,0,4,3,0,142,255,251,247,70,0,4,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,1,0,6,182,255,255,229,40,0,0,142,255,251,248,70,0,4,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,2,0,6,182,255,255,229,42,135,255,251,248,71,0,5,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,2,0,6,182,255,255,246,255,253,248,71,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,0,6,182,255,250,254,248,72,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,2,0,7,180,255,247,73,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,2,0,4,105,55,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,1,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_LYRICS[1024]={
0,0,0,0,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,3,1,0,0,0,0,0,0,0,0,0,0,
0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,9,131,167,164,163,90,
0,0,1,0,52,85,85,86,86,86,86,86,86,86,86,86,86,86,86,85,72,10,0,1,3,0,78,255,255,255,255,255,
0,2,0,139,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,213,32,1,5,0,88,251,248,239,233,167,
3,0,52,255,253,251,252,252,252,252,252,252,252,252,252,252,252,252,252,252,248,255,144,0,8,0,86,255,255,97,16,0,
4,0,88,253,251,255,255,255,255,254,254,254,254,254,254,254,254,255,255,255,250,255,121,0,8,1,86,255,255,83,0,5,
4,0,85,255,252,255,255,255,254,255,255,255,255,255,255,255,255,254,255,252,255,144,8,5,3,0,86,255,255,87,1,4,
4,0,86,255,252,255,255,255,255,239,238,238,238,238,238,237,253,255,253,255,113,0,0,0,10,14,86,255,255,86,0,4,
4,0,86,255,252,255,253,252,99,22,23,21,21,21,22,22,49,219,255,169,0,4,11,140,221,233,226,252,255,86,0,4,
4,0,86,255,252,254,255,243,3,0,0,0,0,0,0,0,0,176,255,40,2,7,188,255,255,255,255,252,255,86,0,4,
4,0,86,255,252,255,253,255,169,93,93,91,91,91,92,91,122,248,211,1,0,97,255,248,253,254,254,252,255,86,0,4,
4,0,86,255,252,255,255,255,255,254,253,253,253,253,253,253,255,255,172,0,0,164,253,251,255,255,255,252,254,87,0,4,
4,0,86,255,252,255,253,252,97,19,20,18,18,18,19,18,44,232,180,1,0,164,253,251,255,255,255,252,255,79,0,4,
4,0,86,255,252,254,255,243,3,0,0,0,0,0,0,0,0,187,224,0,0,97,255,248,253,254,251,253,248,27,0,2,
4,0,86,255,252,255,253,255,169,93,93,91,88,86,88,86,123,246,253,42,2,7,188,255,255,255,255,255,113,0,3,0,
4,0,86,255,252,255,255,255,255,254,253,253,255,255,255,255,255,254,255,169,0,4,11,140,220,232,203,94,0,2,0,0,
4,0,86,255,252,255,253,252,96,19,21,18,46,219,251,250,251,254,253,255,111,0,0,0,5,15,0,0,2,0,0,0,
4,0,86,255,252,254,255,243,4,0,0,0,0,176,255,251,255,255,255,253,255,142,11,3,0,0,0,3,0,0,0,0,
4,0,86,255,252,255,253,255,170,88,90,87,124,246,254,254,255,255,255,255,252,255,198,12,2,2,0,0,0,0,0,0,
4,0,86,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,248,255,108,0,4,0,0,0,0,0,0,
4,0,86,255,252,255,255,255,252,252,252,252,251,254,255,255,255,255,255,255,255,251,251,155,0,4,0,0,0,0,0,0,
4,0,86,255,252,255,255,255,254,254,254,254,254,254,254,254,254,254,254,254,254,248,255,117,0,4,0,0,0,0,0,0,
4,0,86,255,252,255,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,224,21,1,1,0,0,0,0,0,0,
4,0,86,255,252,255,253,255,238,238,238,238,238,238,238,238,238,238,237,236,229,171,34,0,2,0,0,0,0,0,0,0,
4,0,85,255,251,251,255,128,18,20,17,17,17,17,17,17,17,17,17,17,11,0,0,1,0,0,0,0,0,0,0,0,
4,0,89,252,247,255,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,
3,0,65,255,255,127,0,4,2,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,
0,1,0,88,89,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_QUEUE_MUSIC[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,1,0,0,0,
0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,0,
0,0,0,0,0,16,17,17,17,17,17,17,17,17,17,17,17,17,16,0,0,1,0,3,17,17,17,18,2,0,0,0,
0,0,2,3,156,233,232,234,234,234,234,234,234,234,234,234,234,232,233,156,4,2,31,209,237,234,233,233,206,32,1,2,
0,1,0,12,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,15,0,89,255,255,255,255,255,255,80,0,4,
0,0,2,0,85,167,167,168,168,168,168,168,168,168,168,168,168,167,167,85,1,0,85,253,252,197,167,170,131,9,1,1,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,76,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,88,3,7,4,1,0,0,
0,0,2,0,85,167,167,168,168,168,168,168,168,168,168,168,168,167,167,85,1,0,86,255,255,86,0,4,0,0,0,0,
0,1,0,12,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,15,0,87,255,255,86,0,4,0,0,0,0,
0,0,2,3,156,233,232,234,234,234,234,234,234,234,234,234,234,232,233,155,5,0,86,255,255,86,0,4,0,0,0,0,
0,0,0,0,0,20,21,21,21,21,21,21,21,21,18,17,17,17,16,0,6,3,88,255,255,86,0,4,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,78,255,255,86,0,4,0,0,0,0,
0,0,1,0,27,85,86,87,87,87,87,86,87,77,6,1,1,3,0,7,68,80,122,254,255,86,0,4,0,0,0,0,
0,1,1,8,216,255,255,255,255,255,255,255,255,255,139,0,6,0,56,214,255,255,255,252,255,86,0,4,0,0,0,0,
0,1,1,8,216,255,255,255,255,255,255,255,255,255,139,0,2,28,237,255,252,252,254,252,255,86,0,4,0,0,0,0,
0,0,1,0,27,84,85,85,86,86,86,85,86,76,5,3,0,128,255,249,255,255,255,252,255,86,0,4,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,169,254,251,255,255,255,252,254,87,0,4,0,0,0,0,
0,0,0,0,2,4,4,4,4,4,4,4,4,3,1,4,0,151,254,250,255,255,255,252,255,65,0,3,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,60,255,253,250,252,249,255,220,10,1,1,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,121,255,255,255,255,233,55,0,2,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,62,146,164,125,28,0,2,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,3,4,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_DOWNLOAD[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,1,1,9,139,139,9,1,1,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,77,255,255,77,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,88,251,251,88,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,4,0,86,255,255,86,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,3,4,5,0,86,255,255,86,0,5,4,3,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,1,86,255,255,86,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,2,0,103,160,30,0,90,255,255,90,0,29,160,103,0,2,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,0,30,255,255,218,19,73,255,255,73,19,218,255,255,30,0,2,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,1,2,10,189,255,255,204,113,255,255,113,203,255,255,189,9,2,1,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,1,0,13,194,255,255,253,255,255,253,255,255,195,13,0,1,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,1,0,0,2,0,13,194,255,251,255,255,251,255,195,13,0,2,0,0,1,0,0,0,0,0,0,
0,0,0,0,0,2,0,1,0,0,2,0,12,194,255,249,249,255,195,13,0,2,0,0,1,0,2,0,0,0,0,0,
0,0,0,0,0,0,11,0,0,0,0,2,0,13,194,255,255,195,13,0,2,0,0,0,0,11,0,0,0,0,0,0,
0,0,0,3,0,86,230,155,2,2,0,0,2,0,12,164,164,13,0,2,0,0,2,2,156,230,86,0,3,0,0,0,
0,0,0,3,0,175,255,243,17,1,2,1,1,2,0,0,0,0,2,1,1,2,1,17,243,255,175,0,3,0,0,0,
0,0,0,3,0,166,254,232,13,0,0,0,0,0,0,0,0,0,0,0,0,0,0,13,232,254,166,0,3,0,0,0,
0,0,0,3,0,169,254,235,33,17,18,17,17,17,17,17,17,17,17,17,17,18,17,33,235,254,169,0,3,0,0,0,
0,0,0,4,0,159,255,246,235,234,234,234,234,234,234,234,234,234,234,234,234,234,234,235,246,255,159,0,4,0,0,0,
0,0,0,2,1,55,249,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,249,55,1,2,0,0,0,
0,0,0,0,1,0,55,153,167,168,168,168,168,168,168,168,168,168,168,168,168,168,168,167,153,55,0,1,0,0,0,0,
0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,
0,0,0,0,0,0,2,4,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,2,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const unsigned char SB_ICON_HEART[1024]={
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,2,3,4,4,3,2,0,0,0,0,0,0,2,3,4,4,3,2,0,0,0,0,0,0,0,
0,0,0,0,0,1,3,0,0,0,0,0,0,3,2,0,0,1,3,0,0,0,0,0,0,3,1,0,0,0,0,0,
0,0,0,0,2,0,0,31,84,114,113,81,28,0,0,2,2,0,0,27,81,113,114,84,31,0,0,2,0,0,0,0,
0,0,0,2,0,27,162,244,255,255,255,255,241,158,30,0,0,30,158,241,255,255,255,255,244,162,27,0,2,0,0,0,
0,0,2,0,46,227,255,255,252,251,251,252,255,255,233,55,55,233,255,255,252,251,251,252,255,255,227,46,0,2,0,0,
0,1,2,21,226,255,250,254,255,255,255,255,254,251,255,243,243,255,251,254,255,255,255,255,254,250,255,226,21,2,1,0,
0,3,0,147,255,250,255,255,255,255,255,255,255,255,254,255,255,254,255,255,255,255,255,255,255,255,250,255,147,0,3,0,
1,0,16,238,254,254,255,255,255,255,255,255,255,255,255,254,254,255,255,255,255,255,255,255,255,255,254,254,237,15,0,1,
3,0,61,255,253,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,253,255,60,0,3,
4,0,84,254,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,254,84,0,4,
4,0,81,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,255,81,0,4,
3,0,55,255,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,255,55,0,3,
1,0,13,235,255,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,255,235,14,0,1,
0,3,0,161,255,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,255,163,0,3,0,
0,2,0,47,254,252,254,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,254,252,254,49,0,3,0,
0,0,3,0,141,255,250,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,250,255,146,0,3,0,0,
0,0,1,3,6,195,255,251,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,251,255,201,8,3,1,0,0,
0,0,0,2,0,19,213,255,252,255,255,255,255,255,255,255,255,255,255,255,255,255,255,252,255,219,24,0,2,0,0,0,
0,0,0,0,2,0,29,223,255,252,255,255,255,255,255,255,255,255,255,255,255,255,252,255,229,36,0,3,0,0,0,0,
0,0,0,0,0,3,0,36,227,255,252,255,255,255,255,255,255,255,255,255,255,252,255,232,43,0,3,0,0,0,0,0,
0,0,0,0,0,0,3,0,39,226,255,251,255,255,255,255,255,255,255,255,252,255,231,45,0,4,0,0,0,0,0,0,
0,0,0,0,0,0,0,3,0,37,222,255,251,255,255,255,255,255,255,251,255,226,42,0,4,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,3,0,31,213,255,251,255,255,255,255,251,255,215,34,0,4,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,3,0,21,199,255,250,254,254,250,255,200,22,0,3,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,3,0,12,182,255,255,255,255,183,12,0,3,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,2,0,4,154,242,243,154,4,0,2,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,25,25,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
};
static const sb_icon_t sb_icons[] = {
{ SB_ICON_WIFI },
{ SB_ICON_BT },
@@ -212,11 +732,42 @@ static const sb_icon_t sb_icons[] = {
{ SB_ICON_BT_DIS },
{ SB_ICON_BT_SEARCH },
{ SB_ICON_BATT_CHG },
{ SB_ICON_SETTINGS },
{ SB_ICON_REFRESH },
{ SB_ICON_SEARCH },
{ SB_ICON_PLAY_ARROW },
{ SB_ICON_PAUSE },
{ SB_ICON_SKIP_NEXT },
{ SB_ICON_SKIP_PREV },
{ SB_ICON_ARROW_BACK },
{ SB_ICON_CLOSE },
{ SB_ICON_ADD },
{ SB_ICON_CHECK },
{ SB_ICON_LYRICS },
{ SB_ICON_QUEUE_MUSIC },
{ SB_ICON_DOWNLOAD },
{ SB_ICON_HEART },
};
/* 枚举索引(与 sb_icons[] 对应) */
/* 枚举索引(与 sb_icons[] 对应)。状态栏 0-14(勿改序);动作 15-29(TopAppBar 用)。 */
enum { SB_I_WIFI=0, SB_I_BT=1, SB_I_BATT=2, SB_I_VOL=3, SB_I_BRIGHT=4,
SB_I_WIFI_STRONG=5, SB_I_WIFI_MEDIUM=6, SB_I_WIFI_WEAK=7,
SB_I_WIFI_NO=8, SB_I_WIFI_OFF=9, SB_I_BT_NO=10, SB_I_BT_CONN=11,
SB_I_BT_DIS=12, SB_I_BT_SEARCH=13, SB_I_BATT_CHG=14 };
#endif /* SB_ICONS_H */
SB_I_BT_DIS=12, SB_I_BT_SEARCH=13, SB_I_BATT_CHG=14,
SB_I_SETTINGS=15,
SB_I_REFRESH=16,
SB_I_SEARCH=17,
SB_I_PLAY_ARROW=18,
SB_I_PAUSE=19,
SB_I_SKIP_NEXT=20,
SB_I_SKIP_PREV=21,
SB_I_ARROW_BACK=22,
SB_I_CLOSE=23,
SB_I_ADD=24,
SB_I_CHECK=25,
SB_I_LYRICS=26,
SB_I_QUEUE_MUSIC=27,
SB_I_DOWNLOAD=28,
SB_I_HEART=29 };
#endif /* SB_ICONS_H */
+80 -12
View File
@@ -429,6 +429,14 @@ STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in
// for stbi_load_from_file, file pointer is left pointing immediately after image
#endif
// 追加(MeowOS SDK M12):JPEG 解码期降采样。scale>=2 时按 s 抽取输出行/列,
// 不物化整幅原尺寸输出缓冲(*x,*y 返回 ceil(原尺寸/s))。scale<=1 与
// stbi_load / stbi_load_from_memory 完全等价;非 JPEG 格式忽略 scale。
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
#ifndef STBI_NO_STDIO
STBIDEF stbi_uc *stbi_load_scaled (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
#endif
#ifndef STBI_NO_GIF
STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp);
#endif
@@ -907,7 +915,7 @@ typedef struct
#ifndef STBI_NO_JPEG
static int stbi__jpeg_test(stbi__context *s);
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale);
static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp);
#endif
@@ -1134,7 +1142,7 @@ STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_fli
: stbi__vertically_flip_on_load_global)
#endif // STBI_THREAD_LOCAL
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc)
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc, int jpeg_scale)
{
memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields
ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed
@@ -1165,7 +1173,9 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re
// bytes matching expectations; these are prone to false positives, so
// try them later
#ifndef STBI_NO_JPEG
if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);
if (stbi__jpeg_test(s)) return stbi__jpeg_load_ex(s,x,y,comp,req_comp, ri, jpeg_scale);
#else
STBI_NOTUSED(jpeg_scale);
#endif
#ifndef STBI_NO_PNM
if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri);
@@ -1257,10 +1267,10 @@ static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int byt
}
#endif
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
static unsigned char *stbi__load_and_postprocess_8bit_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, int jpeg_scale)
{
stbi__result_info ri;
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8);
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8, jpeg_scale);
if (result == NULL)
return NULL;
@@ -1283,10 +1293,15 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x,
return (unsigned char *) result;
}
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
{
return stbi__load_and_postprocess_8bit_ex(s, x, y, comp, req_comp, 1);
}
static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
{
stbi__result_info ri;
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16);
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16, 1);
if (result == NULL)
return NULL;
@@ -1409,6 +1424,19 @@ STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, i
return result;
}
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load。
STBIDEF stbi_uc *stbi_load_scaled(char const *filename, int *x, int *y, int *comp, int req_comp, int scale)
{
FILE *f = stbi__fopen(filename, "rb");
stbi_uc *result;
stbi__context s;
if (!f) return stbi__errpuc("can't fopen", "Unable to open file");
stbi__start_file(&s,f);
result = stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
if (result) fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);
fclose(f);
return result;
}
#endif //!STBI_NO_STDIO
@@ -1433,6 +1461,14 @@ STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, i
return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);
}
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load_from_memory。
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp, int scale)
{
stbi__context s;
stbi__start_mem(&s,buffer,len);
return stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
}
STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp)
{
stbi__context s;
@@ -1982,6 +2018,7 @@ typedef struct
int nomore; // flag if we saw a marker so must stop
int progressive;
int scale; // 追加(M12):输出抽取步长,默认 1=原尺寸
int spec_start;
int spec_end;
int succ_high;
@@ -3092,6 +3129,12 @@ static void stbi__jpeg_finish(stbi__jpeg *z)
z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data);
}
}
// 追加(M12):该分量系数已全部 IDCT 进像素平面,立即释放,降低进升级 JPEG 的峰值
if (z->img_comp[n].raw_coeff) {
STBI_FREE(z->img_comp[n].raw_coeff);
z->img_comp[n].raw_coeff = 0;
z->img_comp[n].coeff = 0;
}
}
}
}
@@ -3893,6 +3936,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
unsigned int i,j;
stbi_uc *output;
stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL };
stbi_uc *rowbuf = NULL; // 追加(M12):整行临时缓冲,仅 dec>1 时使用
int dec = z->scale > 1 ? z->scale : 1;
int out_w = (z->s->img_x + dec - 1) / dec;
int out_h = (z->s->img_y + dec - 1) / dec;
stbi__resample res_comp[4];
@@ -3918,13 +3965,19 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
else r->resample = stbi__resample_row_generic;
}
// 追加(M12):dec>1 时整行先转成像素再按步长抽取,不物化整幅原尺寸输出缓冲
if (dec > 1) {
rowbuf = (stbi_uc *) stbi__malloc_mad2(n, z->s->img_x, 0);
if (!rowbuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
}
// can't error after this so, this is safe
output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1);
if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
output = (stbi_uc *) stbi__malloc_mad3(n, out_w, out_h, 1);
if (!output) { STBI_FREE(rowbuf); stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
// now go ahead and resample
for (j=0; j < z->s->img_y; ++j) {
stbi_uc *out = output + n * z->s->img_x * j;
stbi_uc *out;
for (k=0; k < decode_n; ++k) {
stbi__resample *r = &res_comp[k];
int y_bot = r->ystep >= (r->vs >> 1);
@@ -3939,6 +3992,9 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
r->line1 += z->img_comp[k].w2;
}
}
// 追加(M12):resample 状态机须逐行推进(上面已完成);非采样行直接丢弃
if (dec > 1 && (j % (unsigned int) dec) != 0) continue;
out = (dec > 1) ? rowbuf : output + (size_t) n * out_w * j;
if (n >= 3) {
stbi_uc *y = coutput[0];
if (z->s->img_n == 3) {
@@ -4016,16 +4072,27 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; }
}
}
// 追加(M12):rowbuf 已按整行转好像素,按步长抽取写入输出行
if (dec > 1) {
stbi_uc *dst = output + (size_t) n * out_w * (j / (unsigned int) dec);
unsigned int x;
for (x=0; x < (unsigned int) out_w; ++x) {
int c;
for (c=0; c < n; ++c)
dst[(size_t) x * n + c] = rowbuf[(size_t) x * dec * n + c];
}
}
}
STBI_FREE(rowbuf);
stbi__cleanup_jpeg(z);
*out_x = z->s->img_x;
*out_y = z->s->img_y;
*out_x = out_w;
*out_y = out_h;
if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output
return output;
}
}
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale)
{
unsigned char* result;
stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg));
@@ -4033,6 +4100,7 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re
memset(j, 0, sizeof(stbi__jpeg));
STBI_NOTUSED(ri);
j->s = s;
j->scale = scale; // 追加(M12):输出抽取步长
stbi__setup_jpeg(j);
result = load_jpeg_image(j, x,y,comp,req_comp);
STBI_FREE(j);
+4 -3
View File
@@ -7,9 +7,10 @@
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_media.c \
* meow_ui_img.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
*/
#include "meow_ui.h"
+189
View File
@@ -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;
}
+466
View File
@@ -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;
}
+69
View File
@@ -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())
+227
View File
@@ -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
+205
View File
@@ -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;
}
+319
View File
@@ -0,0 +1,319 @@
/*
* test_meow_lyric_view.c — meow_lyric_view(SDK 通用歌词页控件)宿主单测(2026-09-03,批2)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global;
* 另链批1 meow_lyric.c 提供歌词数据;meow_lyric_view.c 为被测控件):
* gcc -Wall -Wextra -o test_meow_lyric_view test_meow_lyric_view.c test_host_stub.c \
* meow_lyric.c meow_lyric_view.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_lyric_view
*
* 覆盖矩阵(对齐任务批2:活动行纯白/灰阶、翻译行占位不叠行、偏上锚定、折行中西文、点行 seek 命中、
* 主题换色、scrim 像素渲染):
* L1 活动行纯白 + 上下行逐级灰阶单调递减(dark 主题:onSurface=0xFFFFFF 纯白)
* L2 翻译行占位不叠行:3 行原文+翻译 → 活动行块高 bh=rw+1,下一块始于翻译行之后(块间无重叠)
* L3 偏上锚定:活动行原文块中心 = 视图中线 - above_off(严格偏上)
* L4 折行中西文:长行折成多段(rw>1),且块间按子行高排列
* L5 点行 seek 命中:命中=布局一致(点块内/块外就近取整;点翻译行归本行不误触下一行)
* L6 主题换色:set_colors 覆盖活动行/灰阶/背景;NULL 恢复主题默认
* L7 scrim 像素渲染:draw 后背底像素≈scrim 色(fill_rect_alpha 真实落地)
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
#include "meow_lyric_view.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
/* on_seek 回调收(验证 widget hit 路径回调接线)。 */
static int64_t g_seek_ms = -1;
static void seek_cb(int64_t ms, void *ctx) { (void)ctx; g_seek_ms = ms; }
/* ---- back_buf 像素读取(断言真实渲染) ---- */
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
/* 折行宽测量(宿主):ASCII=1 单位,CJK 多字节=2 单位(中西文断行用)。 */
static int lw(const char *u, int size, void *ctx)
{
(void)size; (void)ctx;
int w = 0;
for (const char *p = u; *p; ) {
unsigned char c = (unsigned char)*p;
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
w += (c < 0x80) ? 1 : 2;
p += ch;
}
return w;
}
/* 亮度(灰度单调性判断用)。 */
static int lum(uint32_t c)
{
return ((c >> 16) & 255) * 3 + ((c >> 8) & 255) * 6 + (c & 255);
}
/*
* 测试歌词:7 行原创 + 两行翻译(与原文同戳合并)。下标:
* 0 First(1000) / 1 Second(2000,tl=第二句翻译) / 2 Third(3000) / 3 Fourth(4000)
* 4 Fifth(5000,tl=第五句翻译长文本折叠测试啊啊啊) / 5 Sixth(6000) / 6 Seventh(7000)
*/
static void load_lyric(void)
{
const char *lrc =
"[00:01.00]First\n"
"[00:02.00]Second\n"
"[00:03.00]Third\n"
"[00:04.00]Fourth\n"
"[00:05.00]Fifth\n"
"[00:06.00]Sixth\n"
"[00:07.00]Seventh\n";
const char *tlrc =
"[00:02.00]\u7b2c\u4e8c\u53e5\u7ffb\u8bd1\n"
"[00:05.00]\u7b2c\u4e94\u53e5\u7ffb\u8bd1\u957f\u6587\u672c\u6298\u53e0\u6d4b\u8bd5\u554a\u554a\u554a\n";
int n = meow_lyric_parse(lrc, tlrc);
assert(n == 7);
/* 翻译合并校验(批1 语义,作前置保证) */
assert(strcmp(meow_lyric_line_tl(1), "\u7b2c\u4e8c\u53e5\u7ffb\u8bd1") == 0);
assert(meow_lyric_line_tl(2)[0] == 0);
}
static void l1_white_gray(void)
{
printf("L1 活动行纯白 + 灰阶单调递减\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
const meow_theme *th = meow_theme_get();
/* 活动行 = 主题 onSurface(dark=0xFFFFFF 纯白) */
uint32_t cur = meow_lyric_view_cur_color(lv);
CHECK(cur == th->onSurface, "cur == th->onSurface (dark=0xFFFFFF 纯白)");
CHECK((cur & 0xFFFFFF) == 0xFFFFFF, "cur 为纯白 0xFFFFFF");
CHECK(meow_lyric_view_line_color(lv, 0) == cur, "line_color(0)==cur");
/* 灰阶单调递减(距活动行越远越灰) */
int mono = 1;
for (int d = 1; d <= 5; d++)
if (lum(meow_lyric_view_line_color(lv, d)) >= lum(meow_lyric_view_line_color(lv, d - 1))) mono = 0;
CHECK(mono, "gray dist 1..5 亮度单调递减");
/* 最远灰阶(对齐 ncm 0x555555) */
CHECK(meow_lyric_view_line_color(lv, 5) == 0x555555, "gray(5)==0x555555 (ncm lyr_gray_tbl)");
meow_lyric_view_free(lv);
}
static void l2_translation_block(void)
{
printf("L2 翻译行占位不叠行(3 行原文 + 翻译)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt > 0, "可见条目数 > 0");
/* 找出当前行(idx1)所在块与下一块 */
int ci = -1, next = -1;
for (int k = 0; k < cnt; k++) {
if (vis[k].ent == 1) ci = k;
else if (ci >= 0 && next < 0) next = k; /* 当前块之后的第一块 */
}
CHECK(ci >= 0, "当前行(Second)在可见块中");
CHECK(vis[ci].bh == vis[ci].rw + 1, "当前行块高 bh=rw+1(原文折行 + 翻译行占 1 子行)");
if (next >= 0) {
/* 下一块顶 = 当前块顶 + 当前块高×子行高(块间无重叠、翻译行不叠进下一行) */
CHECK(vis[next].y == vis[ci].y + vis[ci].bh * 44, "下一块始于翻译行之后(无叠行)");
} else {
/* 当前行是最后可视块:其块高应把翻译行算进去(翻译行占位,否则行会被截断) */
CHECK(vis[ci].y + vis[ci].bh * 44 <= 720, "当前块含翻译行(占用垂直空间)");
}
meow_lyric_view_free(lv);
}
static void l3_above_anchor(void)
{
printf("L3 偏上锚定(活动行块中心在视图中线上方)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */
assert(lv);
meow_lyric_view_set_pos(lv, 5000); /* cur = idx4 Fifth(5 行之后,上方充足) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 5000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt > 0, "可见条目数 > 0");
int ci = -1;
for (int k = 0; k < cnt; k++) if (vis[k].ent == 4) { ci = k; break; }
CHECK(ci >= 0, "当前行(Fifth)在可见块中");
/* 活动行原文块中心 = 块顶 + rw×子行高/2 = 视图中线 - above_off(严格偏上 50px) */
int center = vis[ci].y + vis[ci].rw * 44 / 2;
CHECK(center == 350 - 50, "活动行块中心=350-50=300(偏上锚定 above_off=50)");
CHECK(center < 350, "活动行块中心在视图中线(350)偏上");
meow_lyric_view_free(lv);
}
static void l4_wrap_cjk_west(void)
{
printf("L4 折行中西文(长行折多段,块间按子行高排列)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
/* 窄视图强制折行:w=120 → maxw=120-48=72 单位 */
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 120, 720, lw, NULL);
assert(lv);
/* 用长英文+中文混排行,使当前行折成 >1 段 */
meow_lyric_parse(
"[00:01.00]The quick brown fox jumps over the lazy dog and runs far away into the woods to find shelter from the cold rain today\n"
"[00:05.00]\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u8bba\u6587\u6d4b\u8bd5\u554a\u554a\u554a\u554a\n",
NULL);
meow_lyric_view_set_pos(lv, 1000);
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 1000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt >= 2, "该行折成多段(可见条目含上下行)");
/* 当前行(idx0,长文本)应从 1 段折成多段 */
int ci = -1;
for (int k = 0; k < cnt; k++) if (vis[k].ent == 0) { ci = k; break; }
CHECK(ci >= 0, "当前行在可见块中");
CHECK(vis[ci].rw > 1, "长行折成 rw>1 段(中西文按 词/字符 断行)");
/* 块内子行间距为 sub_lh:子行间不重叠(块高=rw 子行) */
CHECK(vis[ci].bh == vis[ci].rw, "无翻译当前行 bh=rw(子行数=折行段数)");
/* 恢复 7 行歌词(供后续用例) */
load_lyric();
meow_lyric_view_free(lv);
}
static void l5_hit_line(void)
{
printf("L5 点行 seek 命中 = 布局一致\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */
assert(lv);
meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt > 0, "可见条目数 > 0");
/* 命中必须与绘制布局一致:点每个可见块内子行,命中该块 ent */
int consistent = 1;
for (int k = 0; k < cnt && consistent; k++) {
int yb = vis[k].y, yh = vis[k].bh * 44;
for (int dy = 0; dy < yh && consistent; dy += 4) { /* 块内等距采样 */
int hit = meow_lyric_view_hit_line(lv, 240, yb + dy);
if (hit != vis[k].ent) consistent = 0;
}
}
CHECK(consistent, "命中=布局(块内逐点命中 ent 一致)");
/* 点翻译行(当前行块内下一子行)归本行,不误触下一行 */
int ci = -1;
for (int k = 0; k < cnt; k++) if (vis[k].ent == 1) { ci = k; break; }
CHECK(ci >= 0, "当前行在可见块中");
if (ci >= 0 && vis[ci].bh > vis[ci].rw) { /* 有翻译行 */
int ty = vis[ci].y + vis[ci].rw * 44 + 10; /* 翻译子行内 */
CHECK(meow_lyric_view_hit_line(lv, 240, ty) == 1, "点翻译行归本行(Second)不误触下一行");
}
/* 点可见块外(视图内)→ 就近取整(顶部/底部可见行) */
CHECK(meow_lyric_view_hit_line(lv, 240, 101) == vis[0].ent, "视图内顶缝→就近取第一可见块");
CHECK(meow_lyric_view_hit_line(lv, 240, 599) == vis[cnt - 1].ent, "视图内底缝→就近取最后可见块");
/* 视图外 → 未命中 */
CHECK(meow_lyric_view_hit_line(lv, 240, 50) == -1, "视图外(y=50)未命中");
CHECK(meow_lyric_view_hit_line(lv, 490, 300) == -1, "视图外(x=490)未命中");
/* widget 命中路径:命中当前行(有翻译)→ 触发 on_seek 回调 line_time(1)=2000 */
g_seek_ms = -1;
meow_lyric_view_set_on_seek(lv, seek_cb, NULL);
if (ci >= 0) {
int cy = vis[ci].y + 2; /* 当前行原文块内 */
CHECK(meow_widget_hit(meow_lyric_view_widget(lv), 240, cy) == 1, "widget hit 命中当前行");
CHECK(g_seek_ms == 2000, "on_seek 回调 line_time(Second)=2000");
} else {
CHECK(1, "on_seek 校验跳过(无当前行块)");
}
meow_lyric_view_free(lv);
}
static void l6_theme_switch(void)
{
printf("L6 主题换色(set_colors 覆盖 / NULL 恢复默认)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF */
/* 覆盖:活动行红、灰阶自定义、scrim 自定义 */
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
c.scrim = 0x1E1E22; c.cur = 0xFF0000; c.scrim_alpha = 255;
c.gray[0] = 0xFF0000; c.gray[1] = 0xCC0000; c.gray[2] = 0x990000;
c.gray[3] = 0x660000; c.gray[4] = 0x440000; c.gray[5] = 0x220000;
meow_lyric_view_set_colors(lv, &c);
CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "set_colors 覆盖活动行为红");
CHECK(meow_lyric_view_line_color(lv, 3) == 0x660000, "set_colors 覆盖灰阶(3)=0x660000");
CHECK(meow_lyric_view_line_color(lv, 5) == 0x220000, "set_colors 覆盖灰阶(5)=0x220000");
/* 恢复默认:NULL → 回主题 onSurface 纯白 */
meow_lyric_view_set_colors(lv, NULL);
CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 恢复主题默认");
meow_lyric_view_free(lv);
}
static void l7_scrim_pixel(void)
{
printf("L7 scrim 像素渲染(draw 落地)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
/* 不透明 scrim → 背底像素应为 scrim 色(RGB565 舍入容差) */
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
c.scrim = 0x203040; c.scrim_alpha = 255;
meow_lyric_view_set_colors(lv, &c);
meow_lyric_view_set_pos(lv, 2000);
static const char *lrc =
"[00:01.00]First\n[00:02.00]Second\n[00:03.00]Third\n[00:04.00]Fourth\n";
meow_lyric_parse(lrc, NULL);
meow_ui_clear(0);
meow_widget *w = meow_lyric_view_widget(lv);
meow_widget_draw(w);
CHECK(px_near(240, 350, 0x20, 0x30, 0x40), "视图中线像素≈scrim 0x203040(文本打桩不影响 scrim)");
CHECK(px_near(50, 50, 0x20, 0x30, 0x40), "视图左上像素≈scrim 0x203040");
meow_lyric_view_free(lv);
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/lyricview.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(0x53);
load_lyric();
l1_white_gray();
l2_translation_block();
l3_above_anchor();
l4_wrap_cjk_west();
l5_hit_line();
l6_theme_switch();
l7_scrim_pixel();
meow_ui_activity_pop();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+171
View File
@@ -0,0 +1,171 @@
/* test_meow_md3.c — 宿主验证 MD3(Material You)对齐修复(agent-434 P0/P1/P3):
* P0 listview 容器模式(默认 renderer:行=surfaceContainer 圆角卡 + 行间空隙 + 去分隔线);
* P1 按钮胶囊化(圆角=h/2);
* P3 Switch off-track/off-thumb、NavigationBar active pill 64×32、Checkbox/Radio 视觉尺寸。
* 断言全部「画完读 back_buf 像素」(region_has/px_near),不依赖 /dev/fb0/字库(meow_font 打桩)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_md3 test_meow_md3.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_md3
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 12; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* Listview src2 数据源:2 行普通富 item(无图标、无右尾件,label-only)。 */
typedef struct { const char *title; const char *sub; int right; int enabled; } trow_t;
static trow_t g_rows[2] = {
{ "Row 0", NULL, MEOW_LIST_RIGHT_NONE, 1 },
{ "Row 1", NULL, MEOW_LIST_RIGHT_NONE, 1 },
};
static int trow_src(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= 2) return 0;
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = g_rows[idx].title;
out->subtitle = g_rows[idx].sub;
out->right = g_rows[idx].right;
out->on = 0;
out->enabled = g_rows[idx].enabled;
return 1;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/md3.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
assert(meow_ui_width() == 480 && meow_ui_height() == 720);
const meow_theme *th = meow_theme_get();
int sf_r, sf_g, sf_b, sc_r, sc_g, sc_b, p_r, p_g, p_b;
int pc_r, pc_g, pc_b, ov_r, ov_g, ov_b, sch_r, sch_g, sch_b, scs_r, scs_g, scs_b;
UNPK(th->surface, sf_r, sf_g, sf_b);
UNPK(th->surfaceContainer, sc_r, sc_g, sc_b);
UNPK(th->primary, p_r, p_g, p_b);
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
UNPK(th->outline, ov_r, ov_g, ov_b);
UNPK(th->surfaceContainerHighest, sch_r, sch_g, sch_b);
UNPK(th->secondaryContainer, scs_r, scs_g, scs_b);
/* ---- P0 listview 容器模式:行=surfaceContainer 圆角卡 + 空隙,去分隔线 ---- */
meow_widget *lv = meow_listview_create(20, 240, 320, 112, 2, trow_src, NULL, MEOW_LIST_SEL_SINGLE);
assert(lv);
assert(meow_listview_set_item_h(lv, 56) == 56);
assert(meow_listview_set_container(lv, 1) == 0);
meow_ui_clear(th->surface);
meow_widget_draw(lv);
/* 行 0 卡(x=28,y=244,w=304,h=48):中心=surfaceContainer;行 1 同。 */
assert(px_near(180, 268, sc_r, sc_g, sc_b));
assert(px_near(180, 324, sc_r, sc_g, sc_b));
/* 卡间空隙(y=292..300):surface(行间分隔用空隙)。 */
assert(px_near(180, 296, sf_r, sf_g, sf_b));
/* 去分隔线:行边界不再画 outlineVariant(改空隙代替)。 */
assert(!px_near(180, 292, ov_r, ov_g, ov_b));
/* 选中行 0 → primaryContainer(tonal,非满幅实心)。 */
meow_listview_set_sel(lv, 0);
meow_ui_clear(th->surface);
meow_widget_draw(lv);
assert(px_near(180, 268, pc_r, pc_g, pc_b));
assert(px_near(180, 324, sc_r, sc_g, sc_b)); /* 行 1 未选仍卡 */
printf("PASS listview container: rounded card + gap + no-divider + tonal select\n");
/* ---- P1 按钮胶囊化:圆角=h/2(56/2=28)---- */
meow_widget *btn = meow_btn_create_variant(10, 10, 200, 56, "FILLED", MEOW_BTN_FILLED);
assert(btn);
meow_ui_clear(th->surface);
meow_widget_draw(btn);
assert(px_near(110, 22, p_r, p_g, p_b)); /* 上部(避开文字)=primary */
/* 顶缘中心在圆角胶囊内(仍 primary);角落(圆角外)回 surface。 */
assert(px_near(110, 13, p_r, p_g, p_b));
assert(px_near(12, 12, sf_r, sf_g, sf_b)); /* 角落被圆角切掉 */
/* 同高矮按钮(h=40 → 圆角 20):上部仍 primary,角落 surface。 */
meow_widget *btn2 = meow_btn_create_variant(10, 120, 120, 40, "B", MEOW_BTN_FILLED);
assert(btn2);
meow_ui_clear(th->surface);
meow_widget_draw(btn2);
assert(px_near(70, 125, p_r, p_g, p_b));
assert(px_near(12, 122, sf_r, sf_g, sf_b));
printf("PASS button: capsule radius = h/2\n");
/* ---- P3 Switch off-track/off-thumb ---- */
meow_widget *sw = meow_switch_create(20, 300, 0); /* off */
assert(sw);
meow_ui_clear(th->surface);
meow_widget_draw(sw);
assert(px_near(60, 316, sch_r, sch_g, sch_b)); /* off-track=surfaceContainerHighest */
assert(px_near(36, 316, sf_r, sf_g, sf_b)); /* off-thumb=surface(表面色,非恒白) */
meow_switch_set(sw, 1);
meow_ui_clear(th->surface);
meow_widget_draw(sw);
assert(px_near(28, 316, p_r, p_g, p_b)); /* on-track=primary(thumb 左侧) */
printf("PASS switch: off-track surfaceContainerHighest / off-thumb surface\n");
/* ---- P3 NavigationBar active pill 64×32 ---- */
meow_widget *nb = meow_navigationbar_create(0, 600, 480, 80);
assert(nb);
meow_navigationbar_add_item(nb, "A");
meow_navigationbar_add_item(nb, "B");
meow_navigationbar_add_item(nb, "C");
meow_navigationbar_add_item(nb, "D");
meow_navigationbar_set_selected(nb, 0);
meow_ui_clear(th->surface);
meow_widget_draw(nb);
/* item0 pill 中心 (ix+60, y+40)=(60,640) — 64×32 居中于栏;pill 内=secondaryContainer。 */
assert(px_near(60, 640, scs_r, scs_g, scs_b));
/* pill 外(栏上部)= surfaceContainer(navbar 容器底)。 */
assert(px_near(60, 610, sc_r, sc_g, sc_b));
printf("PASS navbar: active pill 64x32 (was 48x22)\n");
/* ---- P3 Checkbox 视觉 24dp(原 34 偏大):勾选框视觉居中,角落回 surface ---- */
meow_widget *ck = meow_checkbox_create(150, 60);
assert(ck);
meow_ui_clear(th->surface);
meow_widget_draw(ck);
/* 未选:24dp 描边框(左缘=150+5=155);框边=outline;内部=surface(空心)。 */
assert(px_near(155, 77, ov_r, ov_g, ov_b)); /* 框左缘 stroke */
assert(px_near(167, 77, sf_r, sf_g, sf_b)); /* 内部空心 */
meow_widget_set_selected(ck, 1);
meow_ui_clear(th->surface);
meow_widget_draw(ck);
/* 选中:24dp 实心 primary 居中(左缘 155..179 顶 65..89);控件矩形角落(视觉 24 外)不填。 */
assert(px_near(150, 60, sf_r, sf_g, sf_b)); /* 视觉 24 外 → surface */
assert(px_near(167, 77, p_r, p_g, p_b)); /* 视觉中心=primary */
printf("PASS checkbox: 24dp visual (was 34)\n");
meow_widget_free(lv); meow_widget_free(btn); meow_widget_free(btn2);
meow_widget_free(sw); meow_widget_free(nb); meow_widget_free(ck);
printf("ALL MD3 PASS\n");
return 0;
}
+111
View File
@@ -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;
}
+133
View File
@@ -0,0 +1,133 @@
/* test_meow_ui_anim.c — §12.2 meow_anim 动画基座宿主验证:
* 缓动数值(tween 中点/终点)、生命周期(start/update/done/cancel)、并行多动画、脏区 flush。
* 时间推进用真实 usleep(meow_ui_time_ms 是单调钟),对中点用宽容差、终点/取消做精确断言。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global):
* gcc -o test_meow_ui_anim test_meow_ui_anim.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_anim
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf 等(脏区 flush 值域校验) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define NEAR(a, b) (((a)-(b)) < 0.0001f && ((b)-(a)) < 0.0001f)
/* ---- 缓动数值 ---- */
static void test_ease(void)
{
CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 0.0f), 0.0f), "lin 0");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 0.5f), 0.5f), "lin .5");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_LINEAR, 1.0f), 1.0f), "lin 1");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OUT, 0.0f), 0.0f), "out 0");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OUT, 1.0f), 1.0f), "out 1");
CHECK(meow_ease_apply(MEOW_EASE_OUT, 0.5f) > 0.5f && meow_ease_apply(MEOW_EASE_OUT, 0.5f) < 1.0f, "out .5 between");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 0.0f), 0.0f), "inout 0");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 0.5f), 0.5f), "inout .5");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_IN_OUT, 1.0f), 1.0f), "inout 1");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OVERSHOOT, 0.0f), 0.0f), "overshoot 0");
CHECK(NEAR(meow_ease_apply(MEOW_EASE_OVERSHOOT, 1.0f), 1.0f), "overshoot 1");
CHECK(meow_ease_apply(MEOW_EASE_OVERSHOOT, 0.85f) > 1.0f, "overshoot mid >1");
}
/* ---- update/done 记录 ---- */
typedef struct { meow_anim *a; float last; int updates; int done; int canceled; } anim_track;
static void on_update(meow_anim *a, float v, void *ctx)
{ anim_track *t = ctx; t->last = v; t->updates++; }
static void on_done(meow_anim *a, int canceled, void *ctx)
{ anim_track *t = ctx; t->a = a; t->done++; t->canceled = canceled; }
static void test_tween_complete(void)
{
anim_track t; memset(&t, 0, sizeof t);
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = 0; d.to = 100; d.duration_ms = 200; d.ease = MEOW_EASE_LINEAR;
d.update = on_update; d.done = on_done; d.ctx = &t;
meow_anim *a = meow_anim_start(&d);
CHECK(a != NULL, "start");
CHECK(meow_anim_active(a) == 1, "active at start");
CHECK(NEAR(meow_anim_value(a), 0.0f), "value from");
CHECK(t.updates == 1 && NEAR(t.last, 0.0f), "update t0");
meow_anim_tick(); /* 与 start 同时刻:仍 ~0 */
CHECK(t.done == 0, "not done yet");
usleep(100000); meow_anim_tick(); /* 中点 */
CHECK(t.last > 10.0f && t.last < 90.0f, "mid value %f", t.last);
usleep(150000); meow_anim_tick(); /* 超过 duration */
CHECK(t.done == 1 && t.canceled == 0, "done once non-cancel");
CHECK(NEAR(t.last, 100.0f), "final value 100 got %f", t.last);
CHECK(meow_anim_active(a) == 0, "inactive after done");
usleep(10000); meow_anim_tick();
CHECK(t.done == 1, "no re-done");
}
static void test_cancel(void)
{
anim_track t; memset(&t, 0, sizeof t);
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = 0; d.to = 100; d.duration_ms = 500; d.ease = MEOW_EASE_LINEAR;
d.update = on_update; d.done = on_done; d.ctx = &t;
meow_anim *a = meow_anim_start(&d);
usleep(50000); meow_anim_tick();
int updates_before = t.updates;
meow_anim_cancel(a);
CHECK(t.done == 1 && t.canceled == 1, "cancel done(canceled=1)");
CHECK(meow_anim_active(a) == 0, "inactive after cancel");
meow_anim_tick();
CHECK(t.updates == updates_before, "no update after cancel");
}
static void test_parallel(void)
{
anim_track t1, t2; memset(&t1, 0, sizeof t1); memset(&t2, 0, sizeof t2);
meow_anim_desc a, b; memset(&a, 0, sizeof a); memset(&b, 0, sizeof b);
a.from = 0; a.to = 10; a.duration_ms = 120; a.ease = MEOW_EASE_LINEAR; a.update = on_update; a.done = on_done; a.ctx = &t1;
b.from = 100; b.to = 0; b.duration_ms = 120; b.ease = MEOW_EASE_LINEAR; b.update = on_update; b.done = on_done; b.ctx = &t2;
meow_anim *x = meow_anim_start(&a);
meow_anim *y = meow_anim_start(&b);
CHECK(x && y, "parallel start");
CHECK(meow_anim_count() == 2, "count 2");
usleep(150000); meow_anim_tick();
CHECK(t1.done == 1 && t2.done == 1, "both done");
CHECK(NEAR(t1.last, 10.0f) && NEAR(t2.last, 0.0f), "endpoints");
CHECK(meow_anim_count() == 0, "count 0");
}
static void test_region_flush(void)
{
anim_track t; memset(&t, 0, sizeof t);
meow_anim_desc d; memset(&d, 0, sizeof d);
d.from = 0; d.to = 1; d.duration_ms = 80; d.ease = MEOW_EASE_LINEAR;
d.update = on_update; d.done = on_done; d.ctx = &t;
meow_anim *a = meow_anim_start(&d);
meow_anim_set_region(a, 100, 200, 60, 40); /* 每 tick 只 flush 此区 */
usleep(100000); meow_anim_tick();
CHECK(t.done == 1, "region anim done");
CHECK(t.updates >= 1, "region updates");
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
test_ease();
test_tween_complete();
test_cancel();
test_parallel();
test_region_flush();
meow_ui_shutdown();
printf("anim: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+231
View File
@@ -0,0 +1,231 @@
/* test_meow_ui_box.c — §12.1 meow_box 线性布局引擎宿主验证:
* 布局求解矩阵(固定/weight/wrap/嵌套/对齐)、margin/padding/gap、交叉轴对齐、view 集成。
* 纯逻辑(只创控件 + arrange + 断言矩形),不依赖 /dev/fb0/字库;不绘制。
*
* 编译(宿主 gcc,链真实 widget/draw/activity/theme/gfx + 打桩 font/global):
* gcc -o test_meow_ui_box test_meow_ui_box.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_box
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_widget_rect:真实几何断言 */
#include "meow_ui_view.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void wr(meow_widget *w, int out[4]) { meow_widget_rect(w, out); }
static void boxr(meow_box *b, int out[4]) { meow_box_rect(b, out); }
/* 创建固定尺寸按钮(既有无绝对定位;注册到绘制物注册表即可) */
static meow_widget *mk(int w, int h)
{
meow_widget *b = meow_btn_create(0, 0, w, h, "X");
return b;
}
/* ---- 竖直列:固定 + padding + gap + CROSS_START ---- */
static void test_col_fixed(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_V);
CHECK(bx != NULL, "box_new V");
meow_box_set_rect(bx, 0, 0, 480, 720);
meow_box_set_padding(bx, 20, 20, 20, 20);
meow_box_set_gap(bx, 10);
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START);
meow_widget *a = mk(100, 50), *b = mk(120, 50), *c = mk(80, 50);
CHECK(meow_box_add(bx, a, MEOW_BOX_FIXED, 0) == 0, "add a");
CHECK(meow_box_add(bx, b, MEOW_BOX_FIXED, 0) == 0, "add b");
CHECK(meow_box_add(bx, c, MEOW_BOX_FIXED, 0) == 0, "add c");
CHECK(meow_box_count(bx) == 3, "count");
meow_box_arrange(bx);
int r[4];
wr(a, r); CHECK(r[0] == 20 && r[1] == 20 && r[2] == 100 && r[3] == 50, "A x=20 y=20 w100 h50 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]);
wr(b, r); CHECK(r[0] == 20 && r[1] == 80 && r[2] == 120, "B y=80 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]);
wr(c, r); CHECK(r[0] == 20 && r[1] == 140 && r[2] == 80, "C y=140 got %d,%d,%d,%d", r[0], r[1], r[2], r[3]);
meow_box_free(bx);
}
/* ---- 横向行:END 对齐 + 交叉轴中心 ---- */
static void test_row_end_center(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_H);
meow_box_set_rect(bx, 10, 0, 480, 200);
meow_box_set_padding(bx, 10, 10, 10, 10);
meow_box_set_gap(bx, 5);
meow_box_set_align(bx, MEOW_BOX_ALIGN_END, MEOW_BOX_CROSS_CENTER);
meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50);
CHECK(meow_box_add(bx, a, MEOW_BOX_FIXED, 0) == 0, "add");
CHECK(meow_box_add(bx, b, MEOW_BOX_FIXED, 0) == 0, "addb");
CHECK(meow_box_add(bx, c, MEOW_BOX_FIXED, 0) == 0, "addc");
meow_box_arrange(bx);
/* 内容区 x∈[20,470] cw=460;foot=100*3+5*2=310;leftover=150;END → off0=150 */
int r[4];
wr(a, r);
CHECK(r[0] == 20 + 150, "END first x=170 got %d", r[0]);
/* 交叉轴中心:cy=10(pad_t);cross_size=ch=180,cross_base=50 → pad=130 → cross=65 → y=10+65=75 */
CHECK(r[1] == 75 && r[3] == 50, "center y=75 got %d,%d", r[1], r[3]);
wr(b, r); CHECK(r[0] == 20 + 150 + 100 + 5, "END second x=%d", r[0]);
wr(c, r); CHECK(r[0] == 380, "END third x=380 got %d", r[0]); /* 170+100+5+100+5+? = 380 */
meow_box_free(bx);
}
/* ---- weight 均分:竖直,两个 weight 1:3 ---- */
static void test_weight(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_H);
meow_box_set_rect(bx, 0, 0, 480, 200);
/* 内容区 480 宽(无 pad/gap)。child 固定 80 + 两个权重 1:3 */
meow_widget *fixed = mk(80, 200), *w1 = mk(0, 200), *w2 = mk(0, 200);
CHECK(meow_box_add(bx, fixed, MEOW_BOX_FIXED, 0) == 0, "fixed");
CHECK(meow_box_add(bx, w1, MEOW_BOX_WEIGHT, 1) == 0, "w1");
CHECK(meow_box_add(bx, w2, MEOW_BOX_WEIGHT, 3) == 0, "w2");
meow_box_arrange(bx);
/* leftover = 480 - 80 = 400;w1=100, w2=300 */
int r[4];
wr(fixed, r); CHECK(r[0] == 0 && r[2] == 80, "fixed x=0 w80 got %d,%d", r[0], r[2]);
wr(w1, r); CHECK(r[0] == 80 && r[2] == 100, "w1 x=80 w100 got %d,%d", r[0], r[2]);
wr(w2, r); CHECK(r[0] == 180 && r[2] == 300, "w2 x=180 w300 got %d,%d", r[0], r[2]);
meow_box_free(bx);
}
/* ---- WRAP 内容测量(measure 回调给 min/preferred) ---- */
static void wrap_measure(void *ctx, int *mn, int *pf) { (void)ctx; *mn = 40; *pf = 120; }
static void test_wrap(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_H);
meow_box_set_rect(bx, 0, 0, 480, 100);
meow_box_set_gap(bx, 10);
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START);
meow_widget *w = mk(5, 40); /* 尺寸无关,wrap 用 measure */
CHECK(meow_box_add_ex(bx, w, MEOW_BOX_WRAP, 0, 40, 120, wrap_measure, NULL) == 0, "wrap-add");
meow_box_arrange(bx);
int r[4]; wr(w, r);
/* wrap 主轴向 preferred=120;cross_start 交叉=40(widget h 落 V=40?这是 H box,交叉=h) */
CHECK(r[2] == 120, "wrap pref w=120 got %d", r[2]);
meow_box_free(bx);
}
/* ---- 嵌套:Row 内嵌 Column ---- */
static void test_nested(void)
{
meow_box *row = meow_box_new(MEOW_BOX_H);
meow_box *col = meow_box_new(MEOW_BOX_V);
meow_box_set_rect(row, 0, 0, 480, 200);
meow_box_set_rect(col, 0, 0, 200, 200);
meow_box_set_gap(row, 10);
meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50);
meow_box_add(row, a, MEOW_BOX_FIXED, 0);
meow_box_add_sub(row, col, MEOW_BOX_FIXED, 0);
CHECK(meow_box_add_sub(row, col, MEOW_BOX_FIXED, 0) < 0, "sub dedupe");
meow_box_add(col, b, MEOW_BOX_FIXED, 0);
meow_box_add(col, c, MEOW_BOX_FIXED, 0);
meow_box_arrange(row);
int r[4];
wr(a, r); CHECK(r[0] == 0 && r[2] == 100, "row a x0 w100 got %d,%d", r[0], r[2]);
int cr[4]; boxr(col, cr); CHECK(cr[0] == 110 && cr[1] == 0 && cr[2] == 200, "sub box x=110 got %d", cr[0]);
/* col 内 b,c 纵向:y 0,50(col y=0, gap0, cross_start) */
meow_widget_rect(b, r); CHECK(r[0] == 110 && r[1] == 0, "col b y0 got %d", r[1]);
meow_widget_rect(c, r); CHECK(r[1] == 50, "col c y50 got %d", r[1]);
meow_box_free(row); meow_box_free(col);
}
/* ---- margin(widget opts)+ 交叉轴 STRETCH ---- */
static void test_margin_stretch(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_V);
meow_box_set_rect(bx, 0, 0, 480, 300);
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_STRETCH);
meow_widget *a = mk(100, 60);
meow_widget_opts o; memset(&o, 0, sizeof o);
o.margin_x = 30; o.margin_y = 10;
CHECK(meow_widget_set_opts(a, &o) == 0, "set opts");
meow_box_add(bx, a, MEOW_BOX_FIXED, 0);
meow_box_arrange(bx);
int r[4]; wr(a, r);
/* V:交叉轴=x,STRETCH → w = cw - 2*margin_x = 480 - 60 = 420;x = margin_x = 30。
* 主轴 y:lead=margin_y=10 → y=10;ext=h=60。 */
CHECK(r[0] == 30 && r[2] == 420, "stretch w=420 x=30 got %d,%d", r[0], r[2]);
CHECK(r[1] == 10 && r[3] == 60, "y=10 h=60 got %d,%d", r[1], r[3]);
meow_box_free(bx);
}
/* ---- SPACE_BETWEEN ---- */
static void test_space_between(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_H);
meow_box_set_rect(bx, 0, 0, 500, 100);
meow_box_set_align(bx, MEOW_BOX_ALIGN_SPACE_BETWEEN, MEOW_BOX_CROSS_START);
meow_widget *a = mk(100, 50), *b = mk(100, 50), *c = mk(100, 50);
meow_box_add(bx, a, MEOW_BOX_FIXED, 0);
meow_box_add(bx, b, MEOW_BOX_FIXED, 0);
meow_box_add(bx, c, MEOW_BOX_FIXED, 0);
meow_box_arrange(bx);
/* cw=500;foot=300;leftover=200;n-1=2 → 每 gap += 100 → A.x=0 B.x=200 C.x=400 */
int r[4];
wr(a, r); CHECK(r[0] == 0, "sb A x0 got %d", r[0]);
wr(b, r); CHECK(r[0] == 200, "sb B x200 got %d", r[0]);
wr(c, r); CHECK(r[0] == 400, "sb C x400 got %d", r[0]);
meow_box_free(bx);
}
/* ---- view 集成:meow_view_set_layout + relayout ---- */
static void test_view_layout(void)
{
meow_view *v = meow_view_create(900, MEOW_VIEW_ACTIVITY, NULL, NULL);
CHECK(v != NULL, "view create");
v->x = 0; v->y = 0; v->w = 480; v->h = 720;
meow_box *bx = meow_box_new(MEOW_BOX_V);
meow_box_set_gap(bx, 8);
meow_box_set_align(bx, MEOW_BOX_ALIGN_START, MEOW_BOX_CROSS_START);
meow_widget *a = mk(300, 50), *b = mk(300, 50);
meow_box_add(bx, a, MEOW_BOX_FIXED, 0);
meow_box_add(bx, b, MEOW_BOX_FIXED, 0);
meow_view_add_widget(v, a);
meow_view_add_widget(v, b);
meow_view_set_layout(v, bx); /* relayout 立即排一次 */
int r[4];
wr(a, r); CHECK(r[0] == 0 && r[1] == 0 && r[2] == 300, "view a x0 y0 w300 got %d,%d,%d", r[0], r[1], r[2]);
wr(b, r); CHECK(r[1] == 58, "view b y=58 got %d", r[1]); /* 0 + 50 + gap8 */
meow_view_free(v);
meow_box_free(bx);
}
/* ---- 越界去重 / 池满 ---- */
static void test_edge(void)
{
meow_box *bx = meow_box_new(MEOW_BOX_H);
meow_widget *a = mk(10, 10), *dup = mk(10, 10);
CHECK(meow_box_add(bx, dup, MEOW_BOX_FIXED, 0) == 0, "add dup");
CHECK(meow_box_add(bx, dup, MEOW_BOX_FIXED, 0) < 0, "dedupe");
CHECK(meow_box_add(bx, NULL, MEOW_BOX_FIXED, 0) < 0, "null add rejected");
meow_box_widget_at(bx, 0) ? 0 : 0;
CHECK(meow_box_widget_at(bx, 0) == dup, "widget_at 0");
CHECK(meow_box_sub_at(bx, 0) == NULL, "sub_at null");
meow_box_free(bx);
}
int main(void)
{
test_col_fixed();
test_row_end_center();
test_weight();
test_wrap();
test_nested();
test_margin_stretch();
test_space_between();
test_view_layout();
test_edge();
printf("box: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+1 -1
View File
@@ -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>,验证持久目录读写
+145
View File
@@ -0,0 +1,145 @@
/* test_meow_ui_dispatch.c — §6.4 事件分发形式化宿主验证(离屏 480x720,16bpp)。
*
* 覆盖(新穿透矩阵,全部直接驱动 SDK §6.4 仲裁者 API + 真实注册控件):
* M1 模态打开点下层不触发:modal_begin 后 meow_ui_hit_topdown 恒返回模态哨兵(不再落到下层
* 同坐标控件),gesture_begin 捕获的是模态(哨兵)而非下层按钮——下层绝不消费;
* M2 弹层叠开两层不叠黑/层隔离:modal_begin 两次 → depth=2;end(LIFO)逐层递减;期间
* hit_topdown 始终返回哨兵,下层收不到任何触摸;
* M3 捕获手势滑出不误触:gesture_begin 命中按钮 A 后捕获;任凭概念滑动到远处(不重新命中),
* meow_ui_gesture_owner 恒为 A(绝不滑出误触下层/邻控件);end 释放;
* M4 list 焦点点外释放:meow_list_tap_at/meow_listview_tap_at 使列表成为唯一焦点;
* focus_tap 落在列表内 → 保位;落在列表外(页面内容/空白)→ 释放焦点并触发 lost 回调。
*
* 编译(宿主 gcc,链真实 widget/draw/input/activity/gfx/sys/theme/vol/log/dialog/ime/notify/ipc +
* 打桩 font/global;完整超集便于链接):
* gcc -Wall -Wextra -o test_meow_ui_dispatch test_meow_ui_dispatch.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_dispatch (无设备,纯宿主)
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "meow_ui_view.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) { printf(" PASS %s\n", name); } \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* 焦点丢失回调计数 */
static int g_lost_calls = 0;
static void *g_lost_last = NULL;
static void focus_lost(void *owner, void *ctx)
{ (void)ctx; g_lost_calls++; g_lost_last = owner; }
/* 列表数据源(最小 W_LIST) */
static int list_src(int idx, char *label, int len, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= 8) return 0;
snprintf(label, (size_t)len, "item %d", idx);
return 1;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/dispatch.ppm", 1);
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
meow_ui_activity_push(5); /* 页面控件 owner=5,命中/绘制对齐 */
/* 场景控件:两个不重叠按钮 A/B + 一个 LazyColumn 列表 */
meow_widget *btnA = meow_btn_create(20, 100, 200, 60, "A");
meow_widget *btnB = meow_btn_create(240, 100, 200, 60, "B");
meow_widget *list = meow_lazycolumn_create(20, 300, 400, 150, 44, 8, list_src, NULL);
/* ================= M1:模态打开点下层不触发 =================
* 无模态:hit_topdown(按钮A中心) == 按钮A;modal_begin 后同一落点返回模态哨兵(≠按钮A),
* gesture_begin 捕获的是模态而非下层按钮——下层绝不消费(穿透根治)。 */
printf("M1 modal open -> lower not triggered\n");
void *h0 = meow_ui_hit_topdown(120, 130);
printf(" no-modal hit_topdown(120,130) = %s\n",
h0 == (void *)btnA ? "btnA" : (h0 ? "other" : "NULL"));
CHECK(h0 == (void *)btnA, "no-modal topdown hits btnA (z-top)");
meow_ui_modal *m = meow_ui_modal_begin();
void *hm = meow_ui_hit_topdown(120, 130);
printf(" modal-open hit_topdown(120,130) = %s (sentinel=%d)\n",
hm == (void *)btnA ? "btnA(LEAK!)" : "modal-sentinel", hm != NULL && hm != (void *)btnA);
CHECK(meow_ui_modal_active(), "modal active");
CHECK(hm != NULL && hm != (void *)btnA, "modal present -> hit_topdown never reaches btnA");
void *g0 = meow_ui_gesture_begin(120, 130); /* 下层同坐标:捕获的是模态而非按钮 */
CHECK(g0 == hm, "gesture_begin during modal captures modal (sentinel), not btnA");
meow_ui_gesture_end();
meow_ui_modal_end(m);
CHECK(meow_ui_hit_topdown(120, 130) == (void *)btnA, "after modal_end topdown returns btnA again");
CHECK(!meow_ui_modal_active(), "modal released");
/* ================= M2:弹层叠开两层(层隔离,depth 语义) =================
* 两层弹开:depth 1→2;期间 hit_topdown 恒返回哨兵;LIFO 关内层→1,关外层→0。
* 叠黑由 Dialog/Sheet 的「打开前整帧快照 + 关闭还原 + 高亮仅重绘卡片不叠遮罩」保证
* (meow_ui_dialog.c,M2.71-bug3),本处验证层隔离 + 栈深。 */
printf("M2 stacked modals (two layers)\n");
meow_ui_modal *m1 = meow_ui_modal_begin();
meow_ui_modal *m2 = meow_ui_modal_begin();
CHECK(meow_ui_modal_depth() == 2, "two modals -> depth 2");
CHECK(meow_ui_hit_topdown(120, 130) != (void *)btnA, "either modal -> lower btnA blocked");
meow_ui_modal_end(m2);
CHECK(meow_ui_modal_depth() == 1, "close inner -> depth 1");
CHECK(meow_ui_hit_topdown(120, 130) != (void *)btnA, "outer modal still blocks lower");
meow_ui_modal_end(m1);
CHECK(meow_ui_modal_depth() == 0, "close outer -> depth 0");
CHECK(meow_ui_hit_topdown(120, 130) == (void *)btnA, "both closed -> lower reachable");
/* ================= M3:捕获手势滑出不误触 =================
* touch-down 命中按钮 A → 捕获。此后 move(即使滑到按钮 B / 空白)不重新命中,owner 恒为 A;
* 绝不滑出误触 B 或下层。抬手(gesture_end)释放。 */
printf("M3 gesture capture: slide-out no false-trigger\n");
void *g1 = meow_ui_gesture_begin(120, 130);
CHECK(g1 == (void *)btnA, "down on A captures A");
CHECK(meow_ui_gesture_owner() == (void *)btnA, "owner == A after down");
/* 概念滑动到按钮 B 中心 (340,130):捕获不重命中 → 仍是 A(不误触 B) */
CHECK(meow_ui_gesture_owner() == (void *)btnA, "slide to B point: owner stays A (no re-hit)");
/* 滑出到空白 */
CHECK(meow_ui_gesture_owner() == (void *)btnA, "slide to blank: owner stays A");
meow_ui_gesture_end();
CHECK(meow_ui_gesture_owner() == NULL, "release -> no owner");
/* ================= M4:list 焦点点外释放 =================
* meow_list_tap_at 使列表成为唯一焦点;focus_tap 落在列表内→保位;落在列表外→释放并通知。 */
printf("M4 list focus release on tap outside\n");
meow_ui_focus_set_lost_cb(focus_lost, NULL);
CHECK(meow_ui_focus_owner() == NULL, "no focus initially");
int idx = meow_list_tap_at(list, 120, 320); /* 命中列表行0 */
CHECK(idx >= 0, "list tap hits a row");
CHECK(meow_ui_focus_owner() == (void *)list, "list tap -> list becomes focus owner");
meow_ui_focus_tap(120, 320); /* 点列表内:保位 */
CHECK(meow_ui_focus_owner() == (void *)list, "tap inside list -> focus retained");
CHECK(g_lost_calls == 0, "no lost callback while focus retained");
meow_ui_focus_tap(120, 130); /* 命中按钮A(非列表):释放焦点+通知 */
CHECK(meow_ui_focus_owner() == NULL, "tap outside list (on A) -> focus released");
CHECK(g_lost_calls == 1 && g_lost_last == (void *)list, "lost callback fired with list owner");
/* 再次聚焦后再点「空白」(无控件命中):同样释放 */
meow_list_tap_at(list, 120, 320);
meow_ui_focus_tap(400, 650); /* 列表外空白 */
CHECK(meow_ui_focus_owner() == NULL, "tap on blank -> focus released");
CHECK(g_lost_calls == 2, "lost callback fired again");
(void)btnB;
/* 清理:控件释放自动注销 z 序绘制物 + 焦点/捕获复位 */
meow_widget_free(list);
meow_widget_free(btnB);
meow_widget_free(btnA);
meow_ui_focus_release(NULL);
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+196
View File
@@ -0,0 +1,196 @@
/* test_meow_ui_home_gesture.c — 批② SDK 全局「App 内底部热区上滑回桌面」宿主验证。
*
* 覆盖(并同 meow_ui_input_is_home_swipe 判据、touch_release 分类、meow_ui_run 分发三层):
* T1 判定矩阵(meow_ui_input_is_home_swipe,镜像 launcher np_gesture.h np_home_gesture):
* 底部热区上滑=1;底部左缘上滑=0(左缘返回互斥);中部上滑=0(App 滚动);底部横向=0;
* 底部下滑=0;上移不足=0;横向主导=0;屏底边界(down_y=fb_h-HOME_EDGE_H)之外=0。
* T2 触摸状态机分类(真实 touch_release → 队列 pop 断言):
* 底部热区上滑→MEOW_EV_HOME(a=MEOW_SWIPE_UP);中部上滑→MEOW_EV_SWIPE(UP)(不被剥回桌面);
* 底部左缘横滑→MEOW_EV_SWIPE(LEFT)(原左缘返回,不被剥)。
* T3 分发(meow_ui_run 主循环,run-loop 复用 test_meow_ui_view_event 的 driver 线程范式):
* 3a 未注册 on_swipe:注入 MEOW_EV_HOME → meow_ui_nav_request_exit 被触发(一次)——回桌面;
* 3b 未注册 on_swipe:注入 MEOW_EV_SWIPE(UP)(中部上滑)→ nav_request_exit 不触发(App 滚动);
* 3c 注册 on_swipe:注入 MEOW_EV_HOME → nav_request_exit 触发(系统级手势优先于 App on_swipe),
* on_swipe 不被调用——屏底热区上滑是系统保留手势,任意 App 内生效(用户 2026-09-04 批②)。
*
* 编译(宿主 gcc,链真实 SDK 源全集 + 打桩 font/global;**不链 meow_ui_nav.c**——本测以测试内
* meow_ui_nav_request_exit 桩计数验证触发):
* gcc -Wall -Wextra -o test_meow_ui_home_gesture test_meow_ui_home_gesture.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_home_gesture
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_input_touch_set/move/release + meow_input_pop + MEOW_EV_* */
#include "meow_ui_nav.h" /* meow_ui_nav_request_exit 原型(本测以桩计数验证) */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
/* MEOW_EV_HOME 是 meow_ui_input.c 内部常量(未入公共头,同 MEOW_EV_NOTIF_PULL 惯例),按同值声明。 */
#ifndef MEOW_EV_HOME
#define MEOW_EV_HOME 9
#endif
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* fb 高度(宿主 480x720) */
#define FBW 480
#define FBH 720
/* 判据常量镜像(np_gesture.h NP_HOME_EDGE_H=28 / NP_HOME_SWIPE=65 / NP_EDGE_W=24)。 */
#define H_EDGE 28
#define H_SWIPE 65
#define EDGE_W 24
/* ---- nav_request_exit 桩(不链 meow_ui_nav.c;计数验证 SDK 触发) ---- */
static int g_nav_exit_calls = 0;
void meow_ui_nav_request_exit(void) { g_nav_exit_calls++; }
/* ---- on_swipe 桩(App 接管验证) ---- */
static int g_swipe_calls = 0;
static int g_swipe_dir = -1;
static int g_swipe_dx = 0;
static void app_on_swipe(int dir, int dx, int dy) { (void)dy; g_swipe_calls++; g_swipe_dir = dir; g_swipe_dx = dx; }
/* 从队列 pop 一个事件并回填 (type,a,b);无事件返回 0。 */
static int pop_ev(int *type, int *a, int *b)
{
meow_input_ev e;
if (!meow_input_pop(&e)) return 0;
*type = e.type; *a = e.a; *b = e.b;
return 1;
}
/* ============ T1 判定矩阵(meow_ui_input_is_home_swipe) ============ */
static void test_predicate(void)
{
printf("T1 meow_ui_input_is_home_swipe matrix\n");
int bz = FBH - H_EDGE; /* 底部热区上边界 y = 720-28 = 692 */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -120, FBH) == 1, "T1 bottom-hotzone up -> 1");
CHECK(meow_ui_input_is_home_swipe(100, FBH - 1, 20, -80, FBH) == 1, "T1 screen-bottom up (x>EDGE_W) -> 1");
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -H_SWIPE, FBH) == 1, "T1 exactly threshold up -> 1");
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, -30, -70, FBH) == 1, "T1 up w/ small left dx (adx<dy) -> 1");
/* 左缘互斥(down_x<EDGE_W) */
CHECK(meow_ui_input_is_home_swipe(10, bz + 10, 0, -120, FBH) == 0, "T1 left-edge origin -> 0 (edge-back reserved)");
/* 中部上滑(App 滚动) */
CHECK(meow_ui_input_is_home_swipe(240, 300, 0, -120, FBH) == 0, "T1 mid-screen up -> 0 (App scroll)");
/* 底部横向(左缘返回/横向逻辑保留) */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 120, 0, FBH) == 0, "T1 bottom horizontal -> 0");
/* 底部下滑 */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, 120, FBH) == 0, "T1 bottom down -> 0");
/* 上移不足(<H_SWIPE) */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -(H_SWIPE - 1), FBH) == 0, "T1 up below threshold -> 0");
/* 竖向未主导(横向位移≥上移量,adx>=dy) */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, -120, -80, FBH) == 0, "T1 up w/ big dx (not vertical-dominant) -> 0");
/* 热区边界之外(down_y < bz) */
CHECK(meow_ui_input_is_home_swipe(240, bz - 1, 0, -120, FBH) == 0, "T1 just above hotzone -> 0");
}
/* ============ T2 触摸状态机分类(touch_release → 队列) ============ */
static void test_touch_classify(void)
{
printf("T2 touch_release classification\n");
meow_input_drain();
/* 底部热区上滑 → MEOW_EV_HOME */
meow_input_touch_set(1, 240, FBH - 5, 0); /* 起点 y=715 ∈ [692,720) */
meow_input_touch_move(240, FBH - 5 - 130); /* 上移 130 */
meow_input_touch_release();
{
int t, a, b;
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_HOME && a == MEOW_SWIPE_UP,
"T2 bottom-hotzone up -> MEOW_EV_HOME(UP) got type=%d a=%d", t, a);
}
/* 中部上滑 → 普通 MEOW_EV_SWIPE(UP)(不被剥回桌面) */
meow_input_touch_set(1, 240, 300, 0);
meow_input_touch_move(240, 300 - 130);
meow_input_touch_release();
{
int t, a, b;
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_SWIPE && a == MEOW_SWIPE_UP,
"T2 mid up -> SWIPE(UP) got type=%d a=%d", t, a);
}
/* 底部左缘横滑 → 原有左缘返回(不被剥) */
meow_input_touch_set(1, 5, FBH - 5, 0);
meow_input_touch_move(5 + 100, FBH - 5);
meow_input_touch_release();
{
int t, a, b;
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_SWIPE && a == MEOW_SWIPE_LEFT,
"T2 bottom-left horizontal -> SWIPE(LEFT) got type=%d a=%d", t, a);
}
}
/* ============ T3 分发(meow_ui_run 主循环) ============ */
static int g_inj_type, g_inj_a, g_inj_b;
static void *driver(void *arg)
{
(void)arg;
usleep(700000); /* 等 startup cooldown(500ms) 过后再注入 */
meow_input_inject(g_inj_type, g_inj_a, g_inj_b);
usleep(80000);
meow_ui_request_exit();
return NULL;
}
static void run_scenario(int inj_type, int inj_a, int inj_b, int register_swipe)
{
g_inj_type = inj_type; g_inj_a = inj_a; g_inj_b = inj_b;
g_nav_exit_calls = 0;
g_swipe_calls = 0; g_swipe_dir = -1; g_swipe_dx = 0;
meow_input_drain(); /* 防上一次残留事件影响本次分发 */
meow_ui_events ev; memset(&ev, 0, sizeof ev);
if (register_swipe) ev.on_swipe = app_on_swipe;
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
meow_ui_run(&ev);
pthread_join(thd, NULL);
}
static void test_dispatch(void)
{
printf("T3 meow_ui_run dispatch (home = nav_request_exit fallback)\n");
/* 3a 未注册 on_swipe:底部热区上滑 → nav_request_exit 触发一次(回桌面) */
run_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 0);
CHECK(g_nav_exit_calls == 1, "T3a no-on_swipe MEOW_EV_HOME -> nav_request_exit fired (got %d)", g_nav_exit_calls);
CHECK(g_swipe_calls == 0, "T3a no-on_swipe -> on_swipe NOT called");
/* 3b 未注册 on_swipe:中部上滑(普通 MEOW_EV_SWIPE UP)→ nav_request_exit 不触发(App 滚动) */
run_scenario(MEOW_EV_SWIPE, MEOW_SWIPE_UP, 120, 0);
CHECK(g_nav_exit_calls == 0, "T3b mid-swipe-up -> nav_request_exit NOT fired (App scroll), got %d", g_nav_exit_calls);
/* 3c 注册 on_swipe:底部热区上滑 → 系统优先,nav_request_exit 触发(回桌面),on_swipe 不被调用 */
run_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 1);
CHECK(g_nav_exit_calls == 1, "T3c on_swipe registered -> nav_request_exit STILL fired (system gesture wins), got %d", g_nav_exit_calls);
CHECK(g_swipe_calls == 0, "T3c system-priority home -> on_swipe NOT called");
}
int main(void)
{
meow_ui_opts o = { FBW, FBH };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
test_predicate();
test_touch_classify();
test_dispatch();
meow_ui_shutdown();
printf("home_gesture: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+121
View File
@@ -0,0 +1,121 @@
/* test_meow_ui_iconbtn.c — M7.0a 缺口#2 宿主验证:IconButton 圆形命中 + 按压态 + 位图图标。
*
* 覆盖:
* I1 圆形命中:中心/边界命中,角部/外缘不命中(区别于矩形按钮);
* I2 owner==active + enabled 双闸门:非 active 或禁用不命中;
* I3 按压态视觉:设 MEOW_WS_PRESSED 后重绘,圆心像素向 onSurface 变亮(与未按不同);
* 禁用时圆心向灰变暗;
* I4 实时按压(meow_input_touch 注入):按下命中圆心 → 圆心变亮;释放恢复。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_ui_iconbtn test_meow_ui_iconbtn.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_iconbtn
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* ---- back_buf 像素读取(断言真实渲染,不依赖目检) ---- */
static void rd_px(int x, int y, unsigned short *p)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *p = 0; return; }
if (bpp == 16) *p = *(unsigned short *)(bb + (size_t)y * line + (size_t)x * 2);
else *p = (unsigned short)(*(unsigned int *)(bb + (size_t)y * line + (size_t)x * 4) >> 16);
}
static unsigned short px565_at(int x, int y)
{
unsigned short p; rd_px(x, y, &p); return p;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/iconbtn.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(7); /* owner 对齐:IconButton 绑定 active=7 */
meow_theme_set_mode(MEOW_THEME_DARK);
/* size=64 → 外接框 (100,100,64,64) → 圆心 (132,132) r=32 */
meow_widget *ic = meow_iconbtn_create(100, 100, 64, MEOW_ICON_PLAY_ARROW);
assert(ic != NULL);
/* ============ I1 圆形命中 ============ */
printf("I1 circular hit\n");
CHECK(meow_widget_hit(ic, 132, 132), "center hits");
CHECK(meow_widget_hit(ic, 100, 132), "left edge (boundary) hits");
CHECK(meow_widget_hit(ic, 132, 100), "top edge (boundary) hits");
CHECK(!meow_widget_hit(ic, 100, 100), "corner (dist>r) MISSES (rect would hit)");
CHECK(!meow_widget_hit(ic, 132, 165), "below center (dist 33>32) MISSES");
/* ============ I2 owner 闸门:切 active 后不命中 ============ */
printf("I2 owner gate\n");
meow_ui_activity_push(8); /* 换 active:original owner=7 ≠ active=8 */
CHECK(!meow_widget_hit(ic, 132, 132), "non-active owner -> MISS (gate)");
meow_ui_activity_pop(); /* 回 active=7 */
CHECK(meow_widget_hit(ic, 132, 132), "back to active -> hits");
/* enabled 闸门 */
meow_widget_set_enabled(ic, 0);
CHECK(!meow_widget_hit(ic, 132, 132), "disabled -> MISS");
meow_widget_set_enabled(ic, 1);
CHECK(meow_widget_hit(ic, 132, 132), "enabled -> hits");
/* ============ I3 按压态视觉(状态机位 + 禁用变暗) ============
* 采样点选 (162,132):圆内 (r=30<32) 但超出图标 ink 右缘(约 x≤148)——取的是圆形底色,
* 而非图标墨色(圆心为图标覆盖,不适合测底色 tint)。 */
printf("I3 pressed visual (state machine / disabled)\n");
const meow_theme *th = meow_theme_get();
meow_ui_clear(th->bg);
unsigned short p_bg = px565_at(100, 100); /* 空屏角部 = 页面底色 */
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0); /* 未按 */
meow_widget_draw(ic);
unsigned short p_idle = px565_at(162, 132);
meow_widget_set_state(ic, MEOW_WS_PRESSED, 1); /* 按下 */
meow_widget_draw(ic);
unsigned short p_press = px565_at(162, 132);
CHECK(p_press != p_idle, "pressed circle-bg pixel differs from idle (tint layer)");
/* 禁用:向灰变暗,圆 bg 应更接近 onSurface 混合(与 idle 不同且更亮/更灰) */
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0);
meow_widget_set_enabled(ic, 0);
meow_widget_draw(ic);
unsigned short p_dis = px565_at(162, 132);
CHECK(p_dis != p_idle, "disabled circle-bg pixel differs from idle (dim)");
meow_widget_set_enabled(ic, 1);
/* 角部(圆外)不应出现圆形底色:等于页面底色(圆形命中/填充不外溢) */
meow_widget_draw(ic);
CHECK(px565_at(100, 100) == p_bg, "corner (outside circle) = page bg (no stray fill)");
/* ============ I4 实时按压(meow_input_touch 注入) ============ */
printf("I4 live press via input touch\n");
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0);
meow_ui_clear(th->bg);
meow_widget_draw(ic);
unsigned short q_idle = px565_at(162, 132);
meow_input_touch_set(1, 132, 132, 0); /* 按在圆心 */
meow_widget_draw(ic);
unsigned short q_live = px565_at(162, 132);
CHECK(q_live != q_idle, "live touch press -> circle bg brightens");
meow_input_touch_set(0, 0, 0, 0); /* 释放 */
meow_widget_draw(ic);
CHECK(px565_at(162, 132) == q_idle, "release -> circle bg restores");
meow_widget_free(ic);
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+278
View File
@@ -0,0 +1,278 @@
/* test_meow_ui_lazygrid.c — 宿主验证 §12.4 2 维虚拟化宫格(meow_lazygrid):
* T1 虚拟化正确性:count/cols/max_scroll;首帧绑定可见行 item;scroll 下滚仅补新入视口的 item;
* 回滚不重绑;全量扫过 = count(每 index 恰 bind 一次);命中把滚动+网格坐标映射回 idx 正确。
* T2 默认 renderer:圆角卡片底 / 标题/副标题 / 选中高亮(primaryContainer)/ 禁用变淡。
* T3 完全自绘 set_item_draw:每可见格调一次 + 自绘内容像素。
* T4 空态占位(count=0 + empty_text 居中)。
* T5 meow_scroll 集成(meow_lazygrid_scroll_attach:track/fling/snap 到行边界)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -o test_meow_ui_lazygrid test_meow_ui_lazygrid.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_lazygrid
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* ---- back_buf 像素读取(真实渲染断言) ---- */
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
static int region_has(int x, int y, int w, int h, int r, int g, int b)
{
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px_near(x + dx, y + dy, r, g, b)) cnt++;
return cnt;
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* ---- 网格几何(测试统一) ---- */
#define GX 20
#define GY 50
#define GW 440
#define GH 200
#define COLS 3
#define CW 100
#define CH 80
#define GAP 10
#define ROWH (CH + GAP) /* 90 */
#define N 20 /* 默认总项数 */
static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表) */
static int g_bind_total; /* bind 回调计数(含 hit 的临时 bind) */
static int g_draw_total; /* draw_cb(自绘)计数 */
static int g_disabled_at; /* 该 index 返回 enabled=0(-1=无) */
static int g_tap_cb_idx; /* tap_at 回调记录的选中项 */
static void lg_tap_cb(int idx, void *ctx) { (void)ctx; g_tap_cb_idx = idx; }
static int lg_count(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; }
static void lg_bind(int idx, meow_lazygrid_item *it, void *ctx)
{
(void)ctx;
g_bind_total++;
memset(it, 0, sizeof *it);
static char t[32], s[32];
snprintf(t, sizeof t, "cell %d", idx);
snprintf(s, sizeof s, "st %d", idx);
it->title = t; it->subtitle = s;
it->enabled = 1;
if (g_disabled_at == idx) it->enabled = 0;
}
/* ---- T3 完全自绘:每格画实心块 + 角标记 ---- */
static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)idx; (void)ctx;
g_draw_total++;
meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义格底 */
meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记 */
if (st & MEOW_LAZYGRID_ST_SELECTED) meow_ui_fill_rect(x + w - 12, y + 4, 8, 8, 0x00FF00);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
int sc_r, sc_g, sc_b, on_r, on_g, on_b, pc_r, pc_g, pc_b, osv_r, osv_g, osv_b, opc_r, opc_g, opc_b;
UNPK(th->surfaceContainer, sc_r, sc_g, sc_b);
UNPK(th->onSurface, on_r, on_g, on_b);
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
UNPK(th->onSurfaceVariant, osv_r, osv_g, osv_b);
UNPK(th->onPrimaryContainer, opc_r, opc_g, opc_b);
g_count_override = -1; g_bind_total = 0; g_disabled_at = -1;
/* ============ T1 虚拟化正确性 ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
CHECK(g != NULL, "create");
CHECK(meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL) == 0, "vsource");
CHECK(meow_lazygrid_max_scroll(g) == 7 * CH + 6 * GAP - GH, "max_scroll %d (expect %d)",
meow_lazygrid_max_scroll(g), 7 * CH + 6 * GAP - GH); /* 620-200=420 */
CHECK(meow_lazygrid_scroll(g) == 0, "scroll0");
meow_ui_clear(th->bg);
meow_lazygrid_draw(g, th);
int B0 = g_bind_total;
int vis_rows = GH / ROWH + 1; /* 3 */
int expect0 = vis_rows * COLS; /* 9(row0..2 × 3 格) */
CHECK(B0 == expect0, "first draw binds %d got %d", expect0, B0);
/* 下滚 1 行(90px):新入视口 = 末 1 行 3 格 → bind +3(其余同槽不重绑) */
meow_lazygrid_set_scroll(g, ROWH);
meow_lazygrid_draw(g, th);
int B1 = g_bind_total;
CHECK(B1 == B0 + COLS, "scroll+1row binds +%d got %d (was %d)", COLS, B1 - B0, B0);
/* 回滚到顶:槽仍在 → 不重绑(此段不调 hit,避免临时 bind 干扰计数) */
meow_lazygrid_set_scroll(g, 0);
meow_lazygrid_draw(g, th);
CHECK(g_bind_total == B1, "scroll-back no rebind got %d (was %d)", g_bind_total, B1);
/* 全量扫过:每个 index 进入视口时 bind 恰一次 → 总 == N(有界,非 per-frame × count) */
int prev = 0;
while (prev <= N) {
int off = prev * ROWH;
meow_lazygrid_set_scroll(g, off);
meow_lazygrid_draw(g, th);
if (off >= meow_lazygrid_max_scroll(g)) break;
prev++;
}
CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N);
/* 命中:索引 = 滚动 + 网格坐标映射(此后的 hit 走临时 bind,不影响上面的总数断言) */
meow_lazygrid_set_scroll(g, 0);
CHECK(meow_lazygrid_hit(g, GX + 10, GY + 10) == 0, "hit cell0 got %d", meow_lazygrid_hit(g, GX + 10, GY + 10));
meow_lazygrid_set_scroll(g, ROWH);
/* scroll=90:GY+10(视口顶)→ 内容 y=100 → row1([90,170])col0 → idx=3 */
CHECK(meow_lazygrid_hit(g, GX + 10, GY + 10) == COLS,
"hit maps scroll+grid got %d (expect 3)", meow_lazygrid_hit(g, GX + 10, GY + 10));
/* gap 区不命中 */
CHECK(meow_lazygrid_hit(g, GX + CW + GAP / 2, GY + 10) == -1, "horizontal gap miss got %d",
meow_lazygrid_hit(g, GX + CW + GAP / 2, GY + 10));
/* 越界列(row0 左侧无格/第 4 列)不命中 */
meow_lazygrid_set_scroll(g, 0);
CHECK(meow_lazygrid_hit(g, GX + COLS * (CW + GAP) + 10, GY + 10) == -1, "col overflow miss got %d",
meow_lazygrid_hit(g, GX + COLS * (CW + GAP) + 10, GY + 10));
CHECK(meow_lazygrid_hit(g, GX + 10, GY + ROWH) == COLS, "hit row1 col0 idx=3 got %d",
meow_lazygrid_hit(g, GX + 10, GY + ROWH));
/* tap_at:命中 → 置选中 + 回调 */
g_tap_cb_idx = -1;
meow_lazygrid_set_on_tap(g, lg_tap_cb, NULL);
int tap_idx = meow_lazygrid_tap_at(g, GX + 50, GY + 40);
CHECK(tap_idx == 0, "tap_at idx0 got %d", tap_idx);
CHECK(meow_lazygrid_sel(g) == 0, "sel set to 0");
CHECK(g_tap_cb_idx == 0, "tap cb idx0 got %d", g_tap_cb_idx);
/* 点 row1,col1(idx=4) → 该格命中 */
int idx4 = meow_lazygrid_hit(g, GX + 1 * (CW + GAP) + 10, GY + ROWH + 10);
CHECK(idx4 == 4, "hit row1col1 idx=%d (expect 4)", idx4);
meow_lazygrid_free(g);
}
/* ============ T2 默认 renderer:卡片底 / 标题 / 选中高亮 / 禁用 ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
g_count_override = 3; g_disabled_at = 2; /* 3 格:cell2 禁用 */
meow_ui_clear(th->bg);
meow_lazygrid_set_sel(g, 0);
meow_lazygrid_draw(g, th);
/* 选中 cell0 → primaryContainer 底([GX,GY,100,80]) */
CHECK(region_has(GX + 4, GY + 4, 90, 60, pc_r, pc_g, pc_b) > 40, "selected cell primaryContainer bg");
/* 未选中 cell1 → surfaceContainer 底(MD3 容器:格内对称内缩 m=8 的圆角卡) */
int x1 = GX + 1 * (CW + GAP);
CHECK(region_has(x1 + 4, GY + 4, 90, 60, sc_r, sc_g, sc_b) > 40, "cell1 surfaceContainer bg");
/* 标题画在 cell0 卡内底部(选中 → onPrimaryContainer;副标题在下方 onPrimaryContainer 同色带)。
* 镜像 lg_draw_cell 的 ty:cy=GY+m、chh=CH-2m、pad=8 → ty = GY+CH-m-pad-thh-(thh+2)。 */
int thh = meow_ui_text_height(th->font_body);
int title_y = GY + CH - 8 - 8 - thh - (thh + 2);
int c0cx = GX + CW / 2;
CHECK(region_has(c0cx - 30, title_y, 60, thh, opc_r, opc_g, opc_b) > 2, "cell0 title drawn (onPrimaryContainer)");
/* 禁用 cell2 变淡:卡内左上角(图标/文本区之外)不再是纯 surfaceContainer(被 onSurface α 叠暗) */
int x2 = GX + 2 * (CW + GAP);
CHECK(!px_near(x2 + 8 + 4, GY + 8 + 4, sc_r, sc_g, sc_b), "disabled cell overlaid (bg dimmed)");
meow_lazygrid_free(g);
}
/* ============ T3 完全自绘(set_item_draw) ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
g_count_override = 3; g_disabled_at = -1; g_draw_total = 0;
CHECK(meow_lazygrid_set_item_draw(g, mydraw, NULL) == 0, "set_item_draw");
meow_ui_clear(th->bg);
meow_lazygrid_draw(g, th);
CHECK(g_draw_total == 3, "draw_cb per visible cell got %d (expect 3)", g_draw_total);
CHECK(region_has(GX + 4, GY + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom cell content drawn");
meow_lazygrid_free(g);
}
/* ============ T4 空态 ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
g_count_override = 0;
meow_lazygrid_set_empty_text(g, "No data");
const meow_theme *t2 = meow_theme_get();
meow_ui_clear(th->bg);
meow_lazygrid_draw(g, t2);
int thh = meow_ui_text_height(t2->font_body);
int ety = GY + (GH - thh) / 2;
CHECK(region_has(GX + GW / 2 - 60, ety, 120, thh, osv_r, osv_g, osv_b) > 2, "empty state text drawn");
meow_lazygrid_free(g);
}
/* ============ T5 meow_scroll 集成(fling/snap) ============ */
{
g_count_override = -1; g_disabled_at = -1;
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
meow_scroll *s = meow_lazygrid_scroll_attach(g);
CHECK(s != NULL, "scroll attach");
meow_scroll_set_snap(s, ROWH);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "drag advances offset got %d", start);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active");
int max_off = start, guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (max_off < cur) max_off = cur;
usleep(10000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(end >= start, "fling advances %d vs %d", end, start);
/* snap 到行边界;若撞底(max_scroll=420 非 row_h 的整数倍)则停在边界(clamp 优先) */
CHECK((end % ROWH) == 0 || end == meow_lazygrid_max_scroll(g),
"fling ends snapped-or-boundary got %d (%%%d, max %d)", end, end % ROWH, meow_lazygrid_max_scroll(g));
CHECK(end <= meow_lazygrid_max_scroll(g), "fling in range %d (max %d)", end, meow_lazygrid_max_scroll(g));
CHECK(meow_lazygrid_scroll(g) == end, "grid scroll mirrors offset");
meow_scroll_free(s);
meow_lazygrid_free(g);
}
meow_ui_shutdown();
printf("lazygrid: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+367
View File
@@ -0,0 +1,367 @@
/* test_meow_ui_listvirt.c — 宿主验证 §5.5 虚拟化列表(W_LISTVIEW pool 模式):
* T1 虚拟化正确性:10000 项滚动窗口内容正确(hit 行号映射)/ bind 次数有界(全量扫过=count)/
* 回滚不重绑(scroll 回顶 bind 计数不变)。
* T2 多行富 item 渲染:双行(标题+副标题)/ 单行(仅标题)/ 左图标占位 / 右尾件 /
* 副标题超宽截断(不越过操作区)。
* T3 段头占位行(bind 返回 section=1)+ 空态占位(count=0 置 empty_text)。
* T4 完全自绘(set_item_draw 覆盖默认 renderer)。
* T5 焦点(tap_at → meow_ui_focus_owner)+ fling 联动(meow_scroll_wrap 包裹 pool 列表,
* track+fling 吸附到行边界;滚动只改 viewport 偏移)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -o test_meow_ui_listvirt test_meow_ui_listvirt.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_listvirt
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* ---- back_buf 像素读取(真实渲染断言) ---- */
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
static int region_has(int x, int y, int w, int h, int r, int g, int b)
{
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px_near(x + dx, y + dy, r, g, b)) cnt++;
return cnt;
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* ---- 虚拟化数据源(10000 项):bind 计数有界 / 内容正确 / 段头 ---- */
#define N 10000
static int g_bind_total; /* pool bind 总数(经 listv_slot 调用;hit 走独立路径不计数) */
static int g_bind_touch_count; /* bind 回调的独立计数(hit 也会调 bind,故仅命中计数用) */
static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表用) */
static int g_section_at; /* 该 index 返回段头(-1=无) */
static int vcount(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; }
static void vbind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
g_bind_total++;
g_bind_touch_count++;
memset(it, 0, sizeof *it);
it->icon_kind = MEOW_LIST_ICON_FOLDER;
it->right = MEOW_LIST_RIGHT_ARROW;
it->on = 1; it->enabled = 1;
if (g_section_at == idx) {
it->section = 1;
it->title = "SECTION A"; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE;
it->right = MEOW_LIST_RIGHT_NONE;
return;
}
static char t[64], s[64];
snprintf(t, sizeof t, "title %d", idx);
snprintf(s, sizeof s, "sub %d", idx);
it->title = t; it->subtitle = s;
}
/* ---- 数据源 A:单行(无副标题/无图标/无尾件),供 T2 单行断言 ---- */
static int via_count(void *ctx) { (void)ctx; return 3; }
static void via_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx; (void)idx;
memset(it, 0, sizeof *it);
static char t[32]; snprintf(t, sizeof t, "single %d", idx);
it->title = t; it->subtitle = NULL; it->icon_kind = MEOW_LIST_ICON_NONE;
it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1;
}
/* ---- 数据源:超宽副标题(T2 截断) ---- */
static int long_count(void *ctx) { (void)ctx; return 1; }
static void long_bind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx; (void)idx;
memset(it, 0, sizeof *it);
it->title = "Title";
it->subtitle = "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"; /* 超宽 */
it->icon_kind = MEOW_LIST_ICON_NONE;
it->right = MEOW_LIST_RIGHT_NONE; it->enabled = 1;
}
/* ---- T4 完全自绘:整行画实心块 + 标题 ---- */
static int g_draw_calls;
static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)idx; (void)ctx;
g_draw_calls++;
meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义行底 */
meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记(自绘内容) */
if (st & MEOW_LISTVIEW_ST_SELECTED) meow_ui_fill_rect(x + w - 12, y + 4, 8, 8, 0x00FF00);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
int sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b, ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b;
UNPK(th->bg, sb_r, sb_g, sb_b);
UNPK(th->onSurface, os_r, os_g, os_b);
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
UNPK(th->onSurfaceVariant, ospv_r, ospv_g, ospv_b);
UNPK(th->primary, prim_r, prim_g, prim_b);
int thh = meow_ui_text_height(th->font_body);
int itemh = 56;
/* ============ T1 虚拟化正确性 / bind 有界 / 回滚不重绑 ============ */
{
g_count_override = -1; g_section_at = -1; g_bind_total = 0; g_bind_touch_count = 0;
int lvx = 20, lvy = 50, lvw = 440, lvh = 300;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h");
CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource");
CHECK(meow_listview_max_scroll(lv) == N * itemh - lvh, "max_scroll %d", meow_listview_max_scroll(lv)); /* 560000-300=559700 */
CHECK(meow_listview_scroll(lv) == 0, "scroll0");
/* 首帧 draw:绑定视口行(first..last) */
meow_widget_draw(lv);
int first0 = 0;
int last0 = (0 + lvh) / itemh + 1; if (last0 > N) last0 = N;
int B0 = g_bind_total;
CHECK(B0 == (last0 - first0), "first draw binds %d rows got %d", last0 - first0, B0);
/* 下滚 1 行:仅新入视口的行被 bind(delta==1),未离屏行不重绑 */
meow_listview_set_scroll(lv, itemh);
meow_widget_draw(lv);
int B1 = g_bind_total;
CHECK(B1 == B0 + 1, "scroll+1row binds exactly 1 more got %d (was %d)", B1 - B0, B0);
/* 回滚到顶:视口行槽位仍在 → 不重绑(delta==0) */
meow_listview_set_scroll(lv, 0);
meow_widget_draw(lv);
CHECK(g_bind_total == B1, "scroll-back no rebind got %d (was %d)", g_bind_total, B1);
/* 全量扫过:每个 index 进入视口时 bind 恰一次 → 总数 == N(有界,非 per-frame × count) */
int prev = 1;
while (prev <= N) {
int off = prev * itemh;
meow_listview_set_scroll(lv, off);
meow_widget_draw(lv);
if (off >= meow_listview_max_scroll(lv)) break;
prev++;
}
meow_listview_set_scroll(lv, meow_listview_max_scroll(lv));
meow_widget_draw(lv);
CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N);
CHECK(g_bind_touch_count == N, "bind callback called %d times (expect %d)", g_bind_touch_count, N);
/* 回滚(扫到末尾后拉回顶):池发生整表 wrap(cap<count),回收合法重绑——但**有界**,
* 每次 draw 至多重绑当前视口窗口(≤ win),绝不整表 N 重绑(可复用回收语义)。 */
int before2 = g_bind_total;
int win = (lvh / itemh) + 1; if (win > N) win = N;
meow_listview_set_scroll(lv, 0);
meow_widget_draw(lv);
CHECK((g_bind_total - before2) <= win, "scroll-back rebind bounded (delta %d ≤ %d)",
g_bind_total - before2, win);
/* 内容正确:命中行号 = 滚动换算(视口窗口映射正确) */
CHECK(meow_listview_hit(lv, lvx + lvw - 10, lvy + 0 * itemh + 20) == 0, "hit0");
meow_listview_set_scroll(lv, 5 * itemh);
int idx_mid = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh / 2);
CHECK(idx_mid == 5, "hit maps scroll to row got %d (expect 5)", idx_mid);
meow_listview_set_scroll(lv, 888 * itemh);
int idx_mid2 = meow_listview_hit(lv, lvx + lvw - 10, lvy + itemh * 2 + itemh / 2);
CHECK(idx_mid2 == 890, "hit row 2 of window got %d (expect 890)", idx_mid2);
meow_widget_free(lv);
}
/* ============ T2 多行富 item 渲染:双行 / 单行 / 图标 / 尾件 / 截断 ============ */
{
/* 双行(lines=2,默认):行 0 title+subtitle + icon(FOLDER) + 尾件(ARROW) */
int lvx = 20, lvy = 50, lvw = 440, lvh = 120;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,本测试专测扁平虚拟化渲染路径 */
CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h");
CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource");
CHECK(meow_listview_set_item_renderer(lv, 2, 1, 1) == 0, "renderer");
meow_ui_clear(th->bg);
meow_widget_draw(lv);
int text_x = lvx + 16 + 24 + 16; /* show_icon=1 → 76 */
int op_start = lvx + (lvw * 2 / 3); /* 20+293=313 */
int ty0 = lvy + (itemh - (2 * thh + 2)) / 2; /* 双行标题 y */
int sy0 = ty0 + thh + 2; /* 副标题 y */
CHECK(region_has(text_x, ty0, 60, thh, os_r, os_g, os_b) > 2, "double-line title drawn");
CHECK(region_has(text_x, sy0, 60, thh, ospv_r, ospv_g, ospv_b) > 2, "double-line subtitle drawn");
CHECK(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 30, "icon placeholder drawn");
int ax = lvx + lvw - 16 - 24;
CHECK(region_has(ax, lvy + itemh / 2 - 8, 20, 16, ospv_r, ospv_g, ospv_b) > 2, "right tail (arrow) drawn");
/* 单行(lines=1):仅标题居中,无副标题带 */
g_count_override = 3;
meow_widget *lv1 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(lv1, itemh);
meow_listview_set_vsource(lv1, via_count, via_bind, NULL);
meow_listview_set_item_renderer(lv1, 1, 0, 0); /* 单行、无图标、无尾件 */
meow_ui_clear(th->bg);
meow_widget_draw(lv1);
int t1y = lvy + (itemh - thh) / 2;
CHECK(region_has(lvx + 16, t1y, 60, thh, os_r, os_g, os_b) > 2, "single-line title drawn");
CHECK(region_has(lvx + 16, t1y + thh + 2, 60, thh, ospv_r, ospv_g, ospv_b) == 0, "single-line no subtitle");
/* 单行档位默认行高 48(set_item_tier) */
meow_widget *lv2 = meow_listview_create(lvx, lvy, lvw, lvh, 3, NULL, NULL, MEOW_LIST_SEL_NONE);
CHECK(meow_listview_set_item_tier(lv2, 1) == 0, "tier");
CHECK(meow_listview_scroll(lv2) == 0 && meow_listview_max_scroll(lv2) == 3 * 48 - lvh, "tier-h %d",
meow_listview_max_scroll(lv2));
meow_widget_free(lv2);
/* 副标题超宽截断:不画过 text_maxw(op_start 之前) */
g_count_override = -1;
meow_widget *lv3 = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(lv3, itemh);
meow_listview_set_vsource(lv3, long_count, long_bind, NULL);
meow_listview_set_item_renderer(lv3, 2, 0, 0); /* 无图标 → text_x = lvx+16=36 */
meow_ui_clear(th->bg);
meow_widget_draw(lv3);
int text_x3 = lvx + 16;
int op3 = lvx + lvw - 16; /* show_right=0 → op_start=右缘-pad */
int maxw3 = op3 - text_x3; /* 20+440-16-36 = 408 */
int ty3 = lvy + (itemh - (2 * thh + 2)) / 2;
int sy3 = ty3 + thh + 2;
CHECK(region_has(text_x3, sy3, 40, thh, ospv_r, ospv_g, ospv_b) > 2, "long subtitle leading drawn");
CHECK(region_has(text_x3 + maxw3 + 4, sy3, 40, thh, ospv_r, ospv_g, ospv_b) == 0, "long subtitle truncated (no overflow)");
meow_widget_free(lv3);
meow_widget_free(lv1);
/* 截断不越界 + 双行副标题带(回归:无图标/无尾件时 text_x/pad 正确) */
CHECK(region_has(text_x3, ty3, 40, thh, os_r, os_g, os_b) > 2, "truncate-r title drawn");
meow_widget_free(lv);
}
/* ============ T3 段头占位 + 空态 ============ */
{
int lvx = 20, lvy = 50, lvw = 440, lvh = 120;
g_count_override = 3; g_section_at = 1;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,段头 band 断言按扁平 */
meow_listview_set_item_h(lv, itemh);
meow_listview_set_vsource(lv, vcount, vbind, NULL);
meow_ui_clear(th->bg);
meow_widget_draw(lv);
int sch_r, sch_g, sch_b; UNPK(th->surfaceContainerHigh, sch_r, sch_g, sch_b);
CHECK(region_has(lvx, lvy + itemh, lvw, itemh - 4, sch_r, sch_g, sch_b) > 40, "section header band drawn");
CHECK(region_has(lvx + 16, lvy + itemh + (itemh - thh) / 2, 60, thh, prim_r, prim_g, prim_b) > 2,
"section header title (primary) drawn");
/* 空态:count=0 + empty_text → 视口内居中画文案(onSurfaceVariant) */
g_count_override = 0; g_section_at = -1;
meow_widget *lve = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_vsource(lve, vcount, vbind, NULL);
meow_listview_set_empty_text(lve, "No data"); /* ASCII(宿主 5x7 点阵仅含 ASCII/CJK 用真机字库) */
meow_ui_clear(th->bg);
meow_widget_draw(lve);
int ety = lvy + (lvh - thh) / 2;
CHECK(region_has(lvx + lvw / 2 - 60, ety, 120, thh, ospv_r, ospv_g, ospv_b) > 2, "empty state text drawn");
meow_widget_free(lve);
meow_widget_free(lv);
}
/* ============ T4 完全自绘(set_item_draw 覆盖默认 renderer) ============ */
{
int lvx = 20, lvy = 200, lvw = 440, lvh = 120;
g_count_override = 3; g_section_at = -1; g_draw_calls = 0;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_item_h(lv, itemh);
meow_listview_set_vsource(lv, vcount, vbind, NULL);
CHECK(meow_listview_set_item_draw(lv, mydraw, NULL) == 0, "set_draw");
meow_ui_clear(th->bg);
meow_widget_draw(lv);
int vis = (lvh / itemh) + 1; if (vis > 3) vis = 3;
CHECK(g_draw_calls == vis, "draw_item called per visible row got %d (expect %d)", g_draw_calls, vis);
CHECK(region_has(lvx + 4, lvy + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom row content drawn");
meow_widget_free(lv);
}
/* ============ T5 焦点 + fling 联动(meow_scroll_wrap 包裹 pool 列表) ============ */
{
int lvx = 20, lvy = 330, lvw = 440, lvh = 300;
g_count_override = -1; g_section_at = -1;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_item_h(lv, itemh);
meow_listview_set_vsource(lv, vcount, vbind, NULL);
/* 焦点:tap_at 命中行 → 列表成为唯一焦点(§6.4) */
int tidx = meow_listview_tap_at(lv, lvx + lvw - 10, lvy + itemh / 2);
CHECK(tidx >= 0, "tap_at returns row got %d", tidx);
CHECK(meow_ui_focus_owner() == (void *)lv, "list acquired focus after tap_at");
/* fling:track+fling → meow_scroll_wrap 包裹;吸附到行边界(snap=itemh) */
meow_scroll *s = meow_scroll_wrap(lv, 1); /* 1=W_LISTVIEW */
CHECK(s != NULL, "scroll wrap pool list");
meow_scroll_set_snap(s, itemh);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "drag advances offset got %d", start);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active");
meow_scroll_stop(s);
CHECK(meow_scroll_active(s) == 0, "stop ok");
/* 再次 fling 到自然停:offset 只改 viewport,且 snap 到行边界 */
for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16);
start = meow_scroll_offset(s);
meow_scroll_fling(s);
int max_off = start, guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (max_off < cur) max_off = cur;
usleep(10000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(end >= start, "fling advances got %d vs %d", end, start);
CHECK((end % itemh) == 0, "snap to item boundary got %d (%%%d)", end, itemh);
CHECK(end <= meow_listview_max_scroll(lv), "fling in range %d (max %d)", end, meow_listview_max_scroll(lv));
/* 滚动只改 viewport:meow_listview_scroll 与物理层一致 */
CHECK(meow_listview_scroll(lv) == end, "listview_scroll mirrors scroll offset %d vs %d", meow_listview_scroll(lv), end);
/* 停稳后 draw 反映最终窗口(bind 只补新入视口的行,不整表重绑) */
int before_draw = g_bind_total;
int win = (lvh / itemh) + 1; if (win > N) win = N; /* 视口窗口行数(含底缘 1 行) */
meow_widget_draw(lv);
CHECK(g_bind_total >= before_draw && (g_bind_total - before_draw) <= win,
"settle draw rebinds only window (delta %d ≤ %d)", g_bind_total - before_draw, win);
meow_scroll_free(s);
meow_widget_free(lv);
}
meow_ui_shutdown();
printf("listvirt: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+396
View File
@@ -0,0 +1,396 @@
/* test_meow_ui_scroll.c — §12.3 meow_scroll 滚动物理宿主验证:
* 跟手拖动(track 跟随与 clamp)、速度跟踪→松手 fling(位移序列/停止位置/单调/不越界)、
* 边界过冲 clamp + 简单回弹、snap。时间推进用真实 usleep(meow_anim_tick 驱动 fling)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/draw/activity/ime + 打桩 font/global):
* gcc -o test_meow_ui_scroll test_meow_ui_scroll.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_scroll
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
#define LIST_H 400
#define ITEM_H 44
#define COUNT 50
/* max_scroll = COUNT*ITEM_H - LIST_H = 2200 - 400 = 1800 */
#define MAXS (COUNT * ITEM_H - LIST_H)
static int src(int idx, char *label, int len, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= COUNT) return 0;
snprintf(label, (size_t)len, "row %d", idx);
return 1;
}
static meow_widget *make_list(void)
{
meow_widget *lv = meow_list_create(0, 0, 480, LIST_H, COUNT, src, NULL);
meow_list_set_item_h(lv, ITEM_H);
return lv;
}
static void pump_until_idle(meow_scroll *s, int *max_off, int *mono_ok)
{
int prev = meow_scroll_offset(s);
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (*max_off < cur) *max_off = cur;
if (cur < prev) *mono_ok = 0; /* 下划 fling 应单调不减(回弹段除外:此处只测无回弹) */
prev = cur;
usleep(10000);
}
meow_anim_tick();
}
static void test_track_follow(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap");
CHECK(meow_scroll_offset(s) == 0, "init offset 0");
meow_scroll_track(s, 10, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 10, "follow +10 got %d", meow_scroll_offset(s));
/* 超量拖到底:clamp 到 max */
meow_scroll_track(s, 100000, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == MAXS, "clamp to max got %d", meow_scroll_offset(s));
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_fling_no_boundary(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
/* 从 0 起拖 ~300px,建正初速 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 25, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "dragged start>0 got %d", start);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active");
int max_off = start, mono = 1, prev = start;
int guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (max_off < cur) max_off = cur;
if (cur < prev) mono = 0;
prev = cur;
usleep(10000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(mono == 1, "monotonic non-decreasing during fling");
CHECK(end >= start, "fling advances end=%d start=%d", end, start);
CHECK(end <= MAXS, "no overflow end=%d max=%d", end, MAXS);
CHECK(meow_scroll_active(s) == 0, "settles idle");
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_fling_boundary_bounce(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
/* 先拖到近底(~1700) */
meow_list_set_scroll(lv, 1700);
/* 快速下划,冲过底界 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 8; i++) meow_scroll_track(s, 40, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int max_off = 0, mono = 1;
pump_until_idle(s, &max_off, &mono);
int end = meow_scroll_offset(s);
CHECK(max_off <= MAXS, "never exceed max, max_off=%d max=%d", max_off, MAXS);
CHECK(end == MAXS, "bounce settles at bottom end=%d", end); /* 回弹后回到底 */
CHECK(meow_scroll_active(s) == 0, "idle after bounce");
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_snap(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
meow_scroll_set_snap(s, ITEM_H); /* 吸附到行边界 */
/* 慢拖到某非边界位置并给 small velocity → fling 结束 snap 到行倍数 */
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
meow_scroll_fling(s);
int max_off = 0, mono = 1;
pump_until_idle(s, &max_off, &mono);
int end = meow_scroll_offset(s);
CHECK((end % ITEM_H) == 0, "snap to item multiple got %d", end);
CHECK(end <= MAXS, "snap in range %d", end);
meow_scroll_free(s);
meow_widget_free(lv);
}
static void test_stop(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 6; i++) meow_scroll_track(s, 20, t + (uint64_t)i * 16);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active (stop test)");
meow_scroll_stop(s);
CHECK(meow_scroll_active(s) == 0, "stopped immediately");
meow_scroll_free(s);
meow_widget_free(lv);
}
/* ============ 池槽归还回归(M19)============
* meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽里占一个;旧实现 free 只归还
* 会话槽、不归还 wrap 槽 → 进程内最多成功 wrap 8 次,之后恒 NULL(图库每次 load_dir 重建,
* 进 8 个目录后列表丢惯性)。以下用例覆盖:长循环回收、free 后重建、交错不串槽、
* 池满仍返回 NULL、直接 create 不误清槽、free(NULL)/二次 free 安全。 */
#define WRAP_POOL 8 /* = meow_ui_scroll.c 的 MEOW_SCROLL_POOL(内部常量,测试按契约固定) */
/* 连续 64 次 wrap/free:每次都要拿到可用会话,且绑定当前列表 */
static void test_wrap_free_recycle(void)
{
meow_widget *lv = make_list();
int ok = 0, first_fail = -1;
for (int i = 0; i < 64; i++) {
meow_scroll *s = meow_scroll_wrap(lv, 0);
if (!s) { if (first_fail < 0) first_fail = i; continue; }
meow_list_set_scroll(lv, 0);
meow_scroll_track(s, 7, meow_ui_time_ms());
if (meow_scroll_offset(s) == 7) ok++;
else if (first_fail < 0) first_fail = i; /* 串槽:set/get 不指向本列表 */
meow_scroll_free(s);
}
CHECK(ok == 64, "64 wrap/free cycles all usable, ok=%d first_fail=%d", ok, first_fail);
meow_widget_free(lv);
}
/* wrap → free → 同一列表再 wrap:必须重新可用 */
static void test_wrap_after_free(void)
{
meow_widget *lv = make_list();
meow_scroll *s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap#1 same list");
meow_scroll_free(s);
s = meow_scroll_wrap(lv, 0);
CHECK(s != NULL, "wrap#2 same list after free");
if (s) {
meow_list_set_scroll(lv, 0);
meow_scroll_track(s, 12, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 12, "re-wrapped scroll works got %d", meow_scroll_offset(s));
meow_scroll_free(s);
}
meow_widget_free(lv);
}
/* 8 个同时活跃 → 第 9 个仍返回 NULL(池满不越界);释放其一的槽立刻可再用 */
static void test_wrap_pool_full(void)
{
meow_widget *lv[WRAP_POOL];
meow_scroll *s[WRAP_POOL];
int ok = 0;
for (int i = 0; i < WRAP_POOL; i++) {
lv[i] = make_list();
s[i] = meow_scroll_wrap(lv[i], 0);
if (s[i]) ok++;
}
CHECK(ok == WRAP_POOL, "all %d wraps active, ok=%d", WRAP_POOL, ok);
meow_widget *extra_lv = make_list();
meow_scroll *extra = meow_scroll_wrap(extra_lv, 0);
CHECK(extra == NULL, "9th simultaneous wrap returns NULL (pool full), got %p", (void *)extra);
/* 释放中间一个 → 槽立即回收;活着的实例仍各自绑定自己的列表 */
meow_scroll_free(s[3]);
s[3] = NULL;
meow_scroll *again = meow_scroll_wrap(extra_lv, 0);
CHECK(again != NULL, "wrap succeeds right after freeing one slot");
if (again) {
meow_list_set_scroll(extra_lv, 0);
meow_scroll_track(again, 9, meow_ui_time_ms());
CHECK(meow_scroll_offset(again) == 9, "new wrap bound to its own list got %d",
meow_scroll_offset(again));
meow_list_set_scroll(lv[5], 33);
CHECK(meow_scroll_offset(s[5]) == 33, "other live wrap keeps its own list got %d",
meow_scroll_offset(s[5]));
meow_scroll_free(again);
}
for (int i = 0; i < WRAP_POOL; i++) {
if (s[i]) meow_scroll_free(s[i]);
meow_widget_free(lv[i]);
}
meow_widget_free(extra_lv);
}
/* 交错:两个 wrap 活跃 → free 其一 → 再 wrap,三者不串槽 */
static void test_wrap_interleaved(void)
{
meow_widget *a = make_list(), *b = make_list(), *c = make_list();
meow_scroll *sa = meow_scroll_wrap(a, 0);
meow_scroll *sb = meow_scroll_wrap(b, 0);
CHECK(sa != NULL && sb != NULL, "two wraps (interleaved) a=%p b=%p", (void *)sa, (void *)sb);
if (!sa || !sb) {
meow_scroll_free(sa); meow_scroll_free(sb);
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
return;
}
meow_list_set_scroll(a, 11);
meow_list_set_scroll(b, 22);
CHECK(meow_scroll_offset(sa) == 11 && meow_scroll_offset(sb) == 22,
"distinct ctx a=%d b=%d", meow_scroll_offset(sa), meow_scroll_offset(sb));
meow_scroll_free(sa);
CHECK(meow_scroll_offset(sb) == 22, "remaining wrap intact after freeing first got %d",
meow_scroll_offset(sb));
meow_scroll *sc = meow_scroll_wrap(c, 0);
CHECK(sc != NULL, "third wrap after freeing first");
if (sc) {
meow_list_set_scroll(c, 0);
meow_scroll_track(sc, 5, meow_ui_time_ms());
CHECK(meow_scroll_offset(sc) == 5, "third wrap bound to its own list got %d",
meow_scroll_offset(sc));
CHECK(meow_scroll_offset(sb) == 22, "second wrap not clobbered by third got %d",
meow_scroll_offset(sb));
meow_scroll_free(sc);
}
meow_scroll_free(sb);
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
}
/* ---- 直接 create(非 wrap)的回调:用于验证 free 不误清别人的 wrap 槽 ---- */
static int g_direct_off;
static void direct_set(void *c, int off) { (void)c; g_direct_off = off; }
static int direct_max(void *c) { (void)c; return 1000; }
static int direct_get(void *c) { (void)c; return g_direct_off; }
static void test_direct_create_free_keeps_wrap_slot(void)
{
meow_widget *lv = make_list();
meow_scroll *w = meow_scroll_wrap(lv, 0);
CHECK(w != NULL, "wrap before direct create");
meow_scroll *d = meow_scroll_create(direct_set, direct_max, direct_get, NULL, NULL);
CHECK(d != NULL, "direct create");
if (d) {
meow_scroll_track(d, 40, meow_ui_time_ms());
CHECK(g_direct_off == 40, "direct scroll tracks got %d", g_direct_off);
meow_scroll_free(d); /* 非 wrap 实例:不得归还/清除任何 wrap 槽 */
}
if (w) {
meow_list_set_scroll(lv, 0);
meow_scroll_track(w, 10, meow_ui_time_ms());
CHECK(meow_scroll_offset(w) == 10, "wrap intact after freeing direct instance got %d",
meow_scroll_offset(w));
meow_scroll_free(w);
}
meow_widget_free(lv);
}
/* free(NULL) 安全;同一实例二次 free 不崩、不清活着的 wrap 槽 */
static void test_wrap_free_null_and_double(void)
{
meow_scroll_free(NULL); /* 必须安全 */
meow_widget *a = make_list(), *b = make_list();
meow_scroll *sa = meow_scroll_wrap(a, 0);
meow_scroll *sb = meow_scroll_wrap(b, 0);
CHECK(sa != NULL && sb != NULL, "two wraps (double free) a=%p b=%p", (void *)sa, (void *)sb);
if (sb) meow_list_set_scroll(b, 44);
if (sa) meow_scroll_free(sa);
if (sa) meow_scroll_free(sa); /* 二次 free:重复清零,不得清掉 sb 的 ctx */
if (sb) {
CHECK(meow_scroll_offset(sb) == 44, "live wrap intact after double free got %d",
meow_scroll_offset(sb));
meow_scroll_free(sb);
}
meow_scroll *sc = meow_scroll_wrap(a, 0);
CHECK(sc != NULL, "wrap still available after free(NULL)/double free");
if (sc) meow_scroll_free(sc);
meow_widget_free(a); meow_widget_free(b);
}
/* 虚拟化列表行源(仅需 count 有效,不绘制) */
static int lv_src2(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= COUNT) return 0;
memset(out, 0, sizeof *out);
out->title = "row";
return 1;
}
/* 槽复用不继承上一个 wrap 的 kind:list → listview → list 交替必须各自生效
* (free 时清 lv 标记,wrap 时按参数重设;否则复用槽上的旧 kind 会让 set/get 打空) */
static void test_wrap_slot_kind_reset(void)
{
meow_widget *plain = make_list();
meow_widget *vl = meow_listview_create(0, 0, 480, LIST_H, COUNT, lv_src2, NULL,
MEOW_LIST_SEL_SINGLE);
meow_listview_set_item_h(vl, ITEM_H);
meow_scroll *s = meow_scroll_wrap(plain, 0);
CHECK(s != NULL, "wrap plain list");
if (s) meow_scroll_free(s);
s = meow_scroll_wrap(vl, 1);
CHECK(s != NULL, "wrap listview on recycled slot");
if (s) {
meow_listview_set_scroll(vl, 0);
meow_scroll_track(s, 20, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 20, "listview wrapped scroll works got %d",
meow_scroll_offset(s));
meow_scroll_free(s);
}
s = meow_scroll_wrap(plain, 0);
CHECK(s != NULL, "wrap plain list again on recycled slot");
if (s) {
meow_list_set_scroll(plain, 0);
meow_scroll_track(s, 15, meow_ui_time_ms());
CHECK(meow_scroll_offset(s) == 15, "plain list after listview slot reuse got %d",
meow_scroll_offset(s));
meow_scroll_free(s);
}
meow_widget_free(vl);
meow_widget_free(plain);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
/* 池槽回归先跑:新建进程里第一个动 pool 的用例,便于对照「修复前 8/64、修复后 64/64」 */
test_wrap_free_recycle();
test_track_follow();
test_fling_no_boundary();
test_fling_boundary_bounce();
test_snap();
test_stop();
test_wrap_after_free();
test_wrap_pool_full();
test_wrap_interleaved();
test_direct_create_free_keeps_wrap_slot();
test_wrap_slot_kind_reset();
test_wrap_free_null_and_double();
meow_ui_shutdown();
printf("scroll: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+178
View File
@@ -0,0 +1,178 @@
/* test_meow_ui_select.c — 宿主验证枚举/预设选择器(meow_select):
* T1 打开→点选项→单选高亮(primaryContainer)+ 选中 + 关闭即回调(on_change(idx))+ 返回值。
* T2 取消:点面板外(遮罩)→ 返回 -1,不调 on_change。
* T3 meow_select_show 一次性:返回选中下标。
*
* 阻塞式模态用后台线程驱动(同 test_ime_fix 范式),主线程经 meow_input_inject 喂触摸。
* 编译(宿主 gcc):
* gcc -o test_meow_ui_select test_meow_ui_select.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_select
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_input_inject + back_buf 读 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* ---- 驱动:后台线程跑阻塞 meow_select_open ---- */
typedef struct {
meow_select *s;
int ret;
} sel_arg;
static void *sel_driver(void *p)
{
sel_arg *a = (sel_arg *)p;
a->ret = meow_select_open(a->s);
return NULL;
}
/* ---- on_change 回调记录 ---- */
static int g_cb = -1;
static void on_change(int idx, void *ctx) { (void)ctx; g_cb = idx; }
/* 计算面板/选项列表几何(与 meow_select_open 同式;用于注入命中坐标 + 像素采样) */
static int sel_list_y(int nitems)
{
const meow_theme *th = meow_theme_get();
int W = meow_ui_width(), H = meow_ui_height();
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
int pad = 24, rowh = 48;
int title_h = meow_ui_text_height(th->font_title);
int avail = H - sb - 20;
int min_h = pad + title_h + 12 + rowh + 20;
int want = min_h;
int est = pad + title_h + 12 + nitems * rowh + 20;
if (est > want) want = est;
if (want > avail) want = avail;
if (want < min_h) want = (min_h < avail) ? min_h : avail;
int py = H - want;
return py + pad + title_h + 12;
}
static int sel_sheet_top(int nitems)
{
const meow_theme *th = meow_theme_get();
int H = meow_ui_height();
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
int pad = 24, rowh = 48;
int title_h = meow_ui_text_height(th->font_title);
int avail = H - sb - 20;
int min_h = pad + title_h + 12 + rowh + 20;
int want = min_h;
int est = pad + title_h + 12 + nitems * rowh + 20;
if (est > want) want = est;
if (want > avail) want = avail;
if (want < min_h) want = (min_h < avail) ? min_h : avail;
return H - want;
}
#define SEL_ROW 48
/* ============ T1 选中 + 回调 + 单选高亮 ============ */
static void test_select_highlight(void)
{
meow_select *s = meow_select_create("Choose", 2);
meow_select_add_item(s, "Alpha");
meow_select_add_item(s, "Beta");
meow_select_add_item(s, "Gamma");
meow_select_set_on_change(s, on_change, NULL);
int list_y = sel_list_y(3);
sel_arg a = { s, -999 };
g_cb = -1;
pthread_t th; pthread_create(&th, NULL, sel_driver, &a);
/* 等 open 的 drain + 首帧绘制完成(高亮已画进 back_buf) */
usleep(150000);
/* 单选高亮:cur=2(第 3 行)选中背景 = primaryContainer */
const meow_theme *thm = meow_theme_get();
int pc_r, pc_g, pc_b; UNPK(thm->primaryContainer, pc_r, pc_g, pc_b);
int hx = 12, hy = list_y + 2 * SEL_ROW + SEL_ROW / 2; /* 容器卡横向内缩 8px(MEOW_LISTV_CARD_MARGIN),探针取卡内 */
int r, g, b; rd_px(hx, hy, &r, &g, &b);
CHECK(near2(r, pc_r) && near2(g, pc_g) && near2(b, pc_b),
"T1 highlight row2 primaryContainer got %02X%02X%02X", r, g, b);
/* 点第 1 行(idx 0)→ 选中 + 关闭 + 回调 */
meow_input_inject(MEOW_EV_TAP, 100, list_y + 0 * SEL_ROW + SEL_ROW / 2);
pthread_join(th, NULL);
CHECK(a.ret == 0, "T1 open return idx0 got %d", a.ret);
CHECK(g_cb == 0, "T1 on_change(0) got %d", g_cb);
meow_select_free(s);
}
/* ============ T2 取消(点面板外) ============ */
static void test_select_cancel(void)
{
meow_select *s = meow_select_create("Choose", 0);
meow_select_add_item(s, "Alpha");
meow_select_add_item(s, "Beta");
meow_select_set_on_change(s, on_change, NULL);
sel_arg a = { s, -999 };
g_cb = -1;
pthread_t th; pthread_create(&th, NULL, sel_driver, &a);
usleep(150000);
int top = sel_sheet_top(2);
meow_input_inject(MEOW_EV_TAP, 240, (top > 30) ? 10 : 10); /* 面板外(遮罩) */
pthread_join(th, NULL);
CHECK(a.ret == -1, "T2 cancel returns -1 got %d", a.ret);
CHECK(g_cb == -1, "T2 on_change NOT fired on cancel got %d", g_cb);
meow_select_free(s);
}
/* ============ T3 meow_select_show 一次性 ============ */
static void *show_driver(void *p)
{
(void)p;
static const char *opts[] = { "A", "B", "C", "D" };
static int r = -999;
r = meow_select_show("Preset", opts, 4, 1);
return (void *)(long)r;
}
static void test_select_show(void)
{
g_cb = -1;
pthread_t th; pthread_create(&th, NULL, show_driver, NULL);
usleep(150000);
int list_y = sel_list_y(4);
meow_input_inject(MEOW_EV_TAP, 100, list_y + 2 * SEL_ROW + SEL_ROW / 2); /* 选第 3 项 idx2 */
long r = 0; pthread_join(th, (void **)&r);
CHECK(r == 2, "T3 show returns idx2 got %ld", r);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
test_select_highlight();
test_select_cancel();
test_select_show();
meow_ui_shutdown();
printf("select: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+174
View File
@@ -0,0 +1,174 @@
/*
* test_meow_ui_str.c — meow_str_* 宿主单测(2026-09-02,§13.i18n 雏形)。
*
* 编译(宿主,meow_ui_str 为纯 libc,无需 SDK 其它对象):
* gcc -Wall -Wextra -o test_meow_ui_str test_meow_ui_str.c meow_ui_str.c -I. -Iinclude
*
* 两种模式(覆盖表按 locale 懒加载,分两次跑):
* ./test_meow_ui_str tmp → unsetenv MEOW_DATA_DIR,仅验证内嵌双语表(无覆盖表)
* ./test_meow_ui_str env → 设 MEOW_DATA_DIR=<mkdtemp>/data,写 <mkdtemp>/strings/en_us.txt,
* 验证 TF 覆盖表优先级(覆盖>内嵌)+ locale 即时生效
*
* 覆盖矩阵:
* T1 内嵌回退(默认 zh_cn):common.ok/loading/no_data 等取 zh
* T2 切 locale 即时生效:set_locale(en_us) → 内嵌 en;set_locale(zh_cn) → 内嵌 zh(无需重初始化)
* T3 未知 id 回退:meow_str("nope.missing") 原样返回 id;meow_str(NULL) 返回 ""
* T4 TF 覆盖优先级:override > 内嵌当前语言;未覆盖 id 回落内嵌;自定义 id 命中
* T5 查找正确性:一批 id 的 zh/en 值逐一断言
* T6 meow_strf:取串并按 snprintf 语义写调用方缓冲(与 meow_str 一致)
*
* 注:set_locale 的 ui.locale 持久化目标是板载 /usr/data/meow/settings.conf,宿主无该目录
* → 静默失败(运行时仍切换,仅不跨启动);不影响本测试断言。
*/
#include "meow_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { printf(" PASS: %s\n", msg); } \
else { printf(" FAIL: %s\n", msg); g_fail = 1; } \
} while (0)
/* 找内嵌某 id 的 zh/en 期望值(供 T5 逐项对照)。不在表内返回 NULL。 */
static const char *expect(const char *id, int en)
{
/* 与 meow_ui_str.c 的 g_embed[] 对齐(此处只列 T5 抽查的子集) */
static const struct { const char *id; const char *zh; const char *en; } t[] = {
{ "common.ok", "确定", "OK" },
{ "common.cancel", "取消", "Cancel" },
{ "common.back", "返回", "Back" },
{ "common.confirm", "确认", "Confirm" },
{ "common.loading", "加载中...", "Loading..." },
{ "common.no_data", "暂无数据", "No data" },
{ "common.retry_fail", "重试失败", "Retry failed" },
{ "common.tap_ok_edit", "点击确定编辑", "tap OK to edit..." },
{ "ime.space", "空格", "Space" },
{ "ime.enter", "确定", "OK" },
{ "ime.esc", "取消", "Cancel" },
{ "ime.clear", "清", "Clear" },
{ "ime.pinyin_mode", "拼音", "Pinyin" },
{ "ime.no_candidate", "(无候选)", "(none)" },
};
for (size_t i = 0; i < sizeof t / sizeof t[0]; i++)
if (strcmp(t[i].id, id) == 0) return en ? t[i].en : t[i].zh;
return NULL;
}
int main(int argc, char **argv)
{
const char *mode = (argc > 1) ? argv[1] : "tmp";
char *tmpdir = NULL;
if (strcmp(mode, "env") == 0) {
/* 建 <mkdtemp>/data + <mkdtemp>/strings/en_us.txt(meow_str 由 MEOW_DATA_DIR 去尾 /data
* 得 <mkdtemp>/,再拼 strings/<locale>.txt)。 */
tmpdir = strdup("/tmp/meowstrtestXXXXXX");
if (!mkdtemp(tmpdir)) { printf("FATAL: mkdtemp failed\n"); return 2; }
char datadir[512], strdir[512];
snprintf(datadir, sizeof datadir, "%s/data", tmpdir);
snprintf(strdir, sizeof strdir, "%s/strings", tmpdir);
mkdir(datadir, 0755);
mkdir(strdir, 0755);
char file[512];
snprintf(file, sizeof file, "%s/strings/en_us.txt", tmpdir);
FILE *f = fopen(file, "w");
if (!f) { printf("FATAL: cannot write override file\n"); free(tmpdir); return 2; }
/* 混合:正常行 / '=' 两侧带空格 / 空行 / 无 '=' 行(应被跳过) */
fprintf(f, "# comment line (no equals)\n");
fprintf(f, "common.ok=确认OK\n");
fprintf(f, "common.custom=自定义覆盖\n");
fprintf(f, "common.loading = 加载中覆盖\n");
fprintf(f, "badline-without-equals\n");
fprintf(f, "\n");
fclose(f);
setenv("MEOW_DATA_DIR", datadir, 1);
} else {
unsetenv("MEOW_DATA_DIR");
}
printf("== meow_str test [%s] ==\n", mode);
printf(" locale = %s\n", meow_str_locale());
/* --- T1 内嵌回退(默认 zh_cn,无覆盖表/无 zh_cn.txt)--- */
CHECK(strcmp(meow_str("common.ok"), "确定") == 0, "T1 zh common.ok = 确定");
CHECK(strcmp(meow_str("common.cancel"), "取消") == 0, "T1 zh common.cancel = 取消");
CHECK(strcmp(meow_str("common.loading"), "加载中...") == 0, "T1 zh common.loading = 加载中...");
CHECK(strcmp(meow_str("common.no_data"), "暂无数据") == 0, "T1 zh common.no_data = 暂无数据");
CHECK(strcmp(meow_str("ime.space"), "空格") == 0, "T1 zh ime.space = 空格");
/* --- T3 未知 id 回退(永不 NULL)--- */
CHECK(strcmp(meow_str("nope.missing"), "nope.missing") == 0, "T3 未知 id 原样返回 id");
CHECK(strcmp(meow_str(NULL), "") == 0, "T3 meow_str(NULL) = \"\"");
/* --- T6 meow_strf:与 meow_str 一致,snprintf 语义 --- */
{
char buf[128];
int r = meow_strf(buf, sizeof buf, "common.ok");
CHECK(r == (int)strlen("确定") && strcmp(buf, "确定") == 0, "T6 meow_strf zh common.ok = 确定");
meow_strf(buf, sizeof buf, "common.no_data");
CHECK(strcmp(buf, meow_str("common.no_data")) == 0, "T6 meow_strf == meow_str");
CHECK(meow_strf(NULL, 10, "common.ok") == -1, "T6 meow_strf(out=NULL) = -1");
CHECK(meow_strf(buf, 0, "common.ok") == -1, "T6 meow_strf(outsz=0) = -1");
}
/* --- T2 切 locale 即时生效(无需重初始化)---
* 用「未在覆盖表」的 id 证明 locale 即时切换(common.ok 等在 env 会被覆盖,留给 T4 验覆盖路径)。 */
meow_str_set_locale("en_us");
CHECK(strcmp(meow_str_locale(), "en_us") == 0, "T2 locale = en_us");
CHECK(strcmp(meow_str("common.cancel"), "Cancel") == 0, "T2 en common.cancel = Cancel");
CHECK(strcmp(meow_str("common.no_data"), "No data") == 0, "T2 en common.no_data = No data");
CHECK(strcmp(meow_str("ime.space"), "Space") == 0, "T2 en ime.space = Space");
/* --- T4 TF 覆盖优先级(仅 env 模式)--- */
if (strcmp(mode, "env") == 0) {
/* ① 覆盖内嵌当前语言(en_us*下 common.ok 应是覆盖值"确认OK"而非内嵌"OK") */
CHECK(strcmp(meow_str("common.ok"), "确认OK") == 0, "T4 @en override 覆盖 common.ok");
/* ② 覆盖自定义 id(内嵌无 → 覆盖命中) */
CHECK(strcmp(meow_str("common.custom"), "自定义覆盖") == 0, "T4 @en override 自定义 id");
/* ③ '=' 两侧带空格也可解析 */
CHECK(strcmp(meow_str("common.loading"), "加载中覆盖") == 0, "T4 @en override 带空格行");
/* ④ 未覆盖 id 回落内嵌当前语言 */
CHECK(strcmp(meow_str("common.cancel"), "Cancel") == 0, "T4 @en 未覆盖 → 内嵌 en");
CHECK(strcmp(meow_str("ime.clear"), "Clear") == 0, "T4 @en ime.clear 未覆盖 → 内嵌 en");
/* ⑤ 自定义 id 在 zh(无 zh_cn.txt,覆盖表清空)→ 内嵌无 → 原样返回 id */
meow_str_set_locale("zh_cn");
CHECK(strcmp(meow_str("common.ok"), "确定") == 0, "T4 @zh 覆盖清除 → 内嵌 zh");
CHECK(strcmp(meow_str("common.custom"), "common.custom") == 0, "T4 @zh 自定义 id 原样回退");
meow_str_set_locale("en_us"); /* 回到 en 供 T5 */
}
/* --- T5 查找正确性:一批 id 的 zh/en 逐一断言(仅 tmp 模式——无覆盖干扰,
* 才能验证内嵌表本身;env 模式的内嵌 vs 覆盖已由 T2/T4 覆盖,此处跳过多余断言)--- */
if (strcmp(mode, "tmp") == 0) {
const char *ids[] = { "common.ok", "common.cancel", "common.back", "common.confirm",
"common.loading", "common.no_data", "common.retry_fail",
"common.tap_ok_edit", "ime.space", "ime.enter", "ime.esc",
"ime.clear", "ime.pinyin_mode", "ime.no_candidate" };
meow_str_set_locale("zh_cn");
for (size_t i = 0; i < sizeof ids / sizeof ids[0]; i++) {
if (expect(ids[i], 0)) {
char msg[128];
snprintf(msg, sizeof msg, "T5 zh %s == 期望", ids[i]);
CHECK(strcmp(meow_str(ids[i]), expect(ids[i], 0)) == 0, msg);
}
}
meow_str_set_locale("en_us");
for (size_t i = 0; i < sizeof ids / sizeof ids[0]; i++) {
if (expect(ids[i], 1)) {
char msg[128];
snprintf(msg, sizeof msg, "T5 en %s == 期望", ids[i]);
CHECK(strcmp(meow_str(ids[i]), expect(ids[i], 1)) == 0, msg);
}
}
}
if (tmpdir) free(tmpdir);
if (g_fail) { printf("\nRESULT: FAIL (%d)\n", g_fail); return 1; }
printf("\nRESULT: ALL PASS\n");
return 0;
}
+210
View File
@@ -0,0 +1,210 @@
/* test_meow_ui_swipe.c — 宿主验证列表行左滑删除(§5.5,M7.3 缺口 #9):
* T1 滑动手势判定与冲突消解:begin 命中可滑行返回 1;横向小位移 = swipe(move 返回 1 消费);
* 纵向大位移 = 滚动手势优先(move 返回 0 并取消本行 swipe);非启用行 begin 返回 0。
* T2 左滑超阈值 → 删除动画(meow_anim 滑出),结束后 on_delete 回调恰一次 + swipe 状态复位。
* T3 未超阈值 → 回弹,on_delete 不触发,swipe_dx 回到 0。
* T4 点按(无位移):begin 后 end(未 started)返回 0(App 走 tap_at),不触发 on_delete。
* T5 右露删除区像素:滑动中重绘列表 → 右露区画主题 error 底 + 行内容左移。
*
* 编译(宿主 gcc):
* gcc -o test_meow_ui_swipe test_meow_ui_swipe.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_swipe
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
static int region_has(int x, int y, int w, int h, int r, int g, int b)
{
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px_near(x + dx, y + dy, r, g, b)) cnt++;
return cnt;
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
#define LVX 20
#define LVY 50
#define LVW 440
#define LVH 300
#define ITEM 56
#define CNT 10
static int del_count; /* on_delete 回调次数 */
static int del_idx; /* 最后一次删除 idx */
static void on_delete(int idx, void *ctx) { (void)ctx; del_count++; del_idx = idx; }
static int scount(void *ctx) { (void)ctx; return CNT; }
static void sbind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
static char t[32];
snprintf(t, sizeof t, "notif %d", idx);
it->title = t; it->icon_kind = MEOW_LIST_ICON_NONE; it->right = MEOW_LIST_RIGHT_NONE;
it->enabled = 1;
}
static meow_widget *make_list(void)
{
meow_widget *lv = meow_listview_create(LVX, LVY, LVW, LVH, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(lv, ITEM);
meow_listview_set_vsource(lv, scount, sbind, NULL);
meow_listview_set_item_renderer(lv, 2, 0, 0); /* 双行、无图标、无尾件 */
meow_listview_set_swipe_delete(lv, 1, on_delete, NULL);
return lv;
}
/* 推进 meow_anim 直到 swipe 动画结束(模拟 on_tick 节拍)。 */
static void pump_swipe(meow_widget *lv)
{
int guard = 0;
while (meow_listview_swipe_active(lv) && guard++ < 80) {
meow_anim_tick();
usleep(10000);
}
meow_anim_tick();
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
int err_r, err_g, err_b; UNPK(th->error, err_r, err_g, err_b);
int row0y = LVY + ITEM / 2; /* 行 0 中心 y(scroll=0) */
/* ============ T1 手势判定 / 冲突消解 ============ */
{
meow_widget *lv = make_list();
/* begin 命中可滑行行 → 1 */
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T1 begin on swipe row = 1");
/* 横向小位移 → 裁定 swipe,move 返回 1(消费) */
CHECK(meow_listview_swipe_move(lv, 40, row0y) == 1, "T1 move horizontal = 1 (consumed)");
int dx = meow_listview_swipe_offset(lv);
CHECK(dx == -60, "T1 dx=-60 got %d", dx);
/* 又一小位移(不超容差也不改变方向) */
CHECK(meow_listview_swipe_move(lv, 30, row0y) == 1, "T1 move horizontal 2 = 1");
CHECK(meow_listview_swipe_end(lv, 30, row0y) == 1, "T1 end started swipe = 1 (bounce)");
pump_swipe(lv);
CHECK(meow_listview_swipe_offset(lv) == 0, "T1 bounce back dx=0 got %d", meow_listview_swipe_offset(lv));
CHECK(del_count == 0, "T1 no delete on bounce");
meow_widget_free(lv);
}
{
/* 纵向大位移 → 滚动手势优先(move 返回 0 + 取消) */
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T1 begin2 = 1");
CHECK(meow_listview_swipe_move(lv, 100, row0y + 80) == 0, "T1 vertical move = 0 (yield to scroll)");
CHECK(meow_listview_swipe_active(lv) == 0, "T1 vertical cancels swipe");
CHECK(meow_listview_swipe_offset(lv) == 0, "T1 vertical dx=0");
meow_widget_free(lv);
}
{
/* 非启用行 begin → 0 */
meow_widget *lv = make_list();
meow_listview_set_swipe_delete(lv, 0, NULL, NULL);
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 0, "T1 disabled begin = 0");
meow_widget_free(lv);
}
/* ============ T2 超阈值 → 删除动画 + on_delete ============ */
{
del_count = 0; del_idx = -1;
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T2 begin = 1");
/* 左滑 200px(阈值=LVW*2/5=176)→ 超阈值 */
CHECK(meow_listview_swipe_move(lv, 100 - 200, row0y) == 1, "T2 move big left = 1");
CHECK(meow_listview_swipe_end(lv, 100 - 200, row0y) == 1, "T2 end = 1 (delete anim)");
CHECK(meow_listview_swipe_active(lv) == 1, "T2 delete anim active");
pump_swipe(lv);
CHECK(del_count == 1, "T2 on_delete fired once got %d", del_count);
CHECK(del_idx == 0, "T2 on_delete idx0 got %d", del_idx);
CHECK(meow_listview_swipe_active(lv) == 0, "T2 state reset inactive");
CHECK(meow_listview_swipe_offset(lv) == 0, "T2 dx reset 0 got %d", meow_listview_swipe_offset(lv));
meow_widget_free(lv);
}
/* ============ T3 未超阈值 → 回弹,on_delete 不触发 ============ */
{
del_count = 0;
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T3 begin = 1");
CHECK(meow_listview_swipe_move(lv, 55, row0y) == 1, "T3 move left 45px = 1");
CHECK(meow_listview_swipe_offset(lv) == -45, "T3 dx=-45 got %d", meow_listview_swipe_offset(lv));
CHECK(meow_listview_swipe_end(lv, 55, row0y) == 1, "T3 end = 1 (bounce anim)");
CHECK(meow_listview_swipe_active(lv) == 1, "T3 bounce active");
pump_swipe(lv);
CHECK(del_count == 0, "T3 on_delete NOT fired got %d", del_count);
CHECK(meow_listview_swipe_offset(lv) == 0, "T3 bounce dx=0 got %d", meow_listview_swipe_offset(lv));
CHECK(meow_listview_swipe_active(lv) == 0, "T3 inactive after bounce");
meow_widget_free(lv);
}
/* ============ T4 点按(无位移)= 行点击,end 返回 0 ============ */
{
del_count = 0;
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T4 begin = 1");
CHECK(meow_listview_swipe_end(lv, 100, row0y) == 0, "T4 tap end = 0 (let App tap_at)");
CHECK(del_count == 0, "T4 no delete on tap");
CHECK(meow_listview_swipe_active(lv) == 0, "T4 inactive after tap");
meow_widget_free(lv);
}
/* ============ T5 右露删除区像素(默认 renderer) ============ */
{
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T5 begin = 1");
CHECK(meow_listview_swipe_move(lv, 100 - 200, row0y) == 1, "T5 move big left = 1");
meow_ui_clear(th->bg);
meow_widget_draw(lv);
/* 右露区 [LVX+LVW+dx, row0, -dx, ITEM] = [260,50,200,56] → error 底 */
CHECK(region_has(270, row0y - ITEM / 2 + 4, 180, ITEM - 8, err_r, err_g, err_b) > 50,
"T5 delete reveal error bg got %d", region_has(270, row0y - ITEM / 2 + 4, 180, ITEM - 8, err_r, err_g, err_b));
meow_listview_swipe_end(lv, 100 - 200, row0y);
pump_swipe(lv);
meow_widget_free(lv);
}
meow_ui_shutdown();
printf("swipe: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+4 -3
View File
@@ -9,9 +9,10 @@
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -o test_meow_ui_theme test_meow_ui_theme.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_img.c meow_ui_media.c \
* meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
*
* 验证要点:四套主题 primary/radius 各不相同(MD3 蓝 / MIUIx 橙 + 更大圆角),
* 同 style 下 light/dark 的 surface 成对切换;图形用 MEOW_UI_DUMP 导出 PPM 目检。
+122
View File
@@ -0,0 +1,122 @@
/* test_meow_ui_theme_export.c — M7.0a 主题收口前置(缺口#6 SDK 半)宿主验证:
* §8.1.5 色板导出/注入——meow_theme_export 拷当前权威主题;meow_theme_inject 覆写当前生效主题。
*
* 覆盖:
* T1 导出一致性:export 后关键字段 == meow_theme_get()(当前 style×mode 权威板);
* T2 注入:inject 一份自定义板 → meow_theme_get()/export 都返回注入板(单一权威反映);
* 注入恢复内建:inject(NULL) 后回到目标 style×mode 的内建板;
* T3 launcher 薄壳映射契约:导出板能映射出 meow_ui_theme_t 的 9 个角色字段(surface/
* surface_variant/surface_container/primary/on_surface/on_surface_variant/outline/error/
* surface_tint),值与 SDK meow_theme 对应字段一致。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_ui_theme_export test_meow_ui_theme_export.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_theme_export
*/
#include "meow_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* launcher meow_ui_theme_t 的 9 字段形状(测试内复刻字段名,不依赖 launcher 内部头)。 */
typedef struct {
uint32_t surface, surface_variant, surface_container;
uint32_t primary, on_surface, on_surface_variant;
uint32_t outline, error, surface_tint;
} shell_theme;
static shell_theme export_shell(const meow_theme *t)
{
shell_theme s;
s.surface = t->surface;
s.surface_variant = t->surfaceVariant;
s.surface_container = t->surfaceContainer;
s.primary = t->primary;
s.on_surface = t->onSurface;
s.on_surface_variant= t->onSurfaceVariant;
s.outline = t->outline;
s.error = t->error;
s.surface_tint = t->surfaceTint;
return s;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/theme_export.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
/* ---- T1 导出一致性(MD3 dark) ---- */
printf("T1 export consistency\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_ui_set_style(MEOW_STYLE_MD3);
const meow_theme *t0 = meow_theme_get();
meow_theme e0; meow_theme_export(&e0);
CHECK(e0.surface == t0->surface, "export.surface == get.surface");
CHECK(e0.primary == t0->primary, "export.primary == get.primary");
CHECK(e0.surfaceVariant== t0->surfaceVariant,"export.surfaceVariant == get.surfaceVariant");
CHECK(e0.onSurface == t0->onSurface, "export.onSurface == get.onSurface");
CHECK(e0.radius == t0->radius, "export.radius == get.radius");
CHECK(e0.bg == t0->bg, "export.bg == get.bg (background override applied)");
/* MIUIx light:确认导出随 style×mode 切换(不是固定板) */
meow_ui_set_style(MEOW_STYLE_MIUIX);
meow_theme_set_mode(MEOW_THEME_LIGHT);
const meow_theme *t1 = meow_theme_get();
meow_theme e1; meow_theme_export(&e1);
CHECK(e1.primary == 0xF57200 && t1->primary == 0xF57200, "MIUIx light primary exported");
CHECK(e1.bg == t1->bg && e1.surface == t1->surface, "MIUIx light bg/surface exported");
CHECK(e1.surface == (uint32_t)0xF7F3EE, "MIUIx light surface == warm default");
/* ---- T2 注入覆写 + 恢复内建 ---- */
printf("T2 inject override + restore\n");
meow_theme custom; memset(&custom, 0, sizeof custom);
custom.surface = 0x112233; custom.primary = 0xABCDEF; custom.onSurface = 0x0F0F0F;
custom.radius = 99; custom.radius_card = 100;
meow_theme_inject(&custom);
const meow_theme *ti = meow_theme_get();
meow_theme ei; meow_theme_export(&ei);
CHECK(ti->primary == 0xABCDEF && ti->surface == 0x112233 && ti->radius == 99,
"inject -> get() reads injected palette");
CHECK(ei.primary == 0xABCDEF && ei.surface == 0x112233, "inject -> export reads injected palette");
/* meow_theme_export == meow_theme_get even when injected(单一权威) */
CHECK(ei.onSurface == ti->onSurface && ei.radius_card == ti->radius_card,
"export == get while injected");
/* 注入恢复内建 */
meow_theme_inject(NULL);
const meow_theme *tr = meow_theme_get();
CHECK(tr->primary == 0xF57200 && tr->surface == 0xF7F3EE,
"inject(NULL) restores builtin (MIUIx light)");
/* ---- T3 launcher 薄壳映射契约(9 角色字段) ---- */
printf("T3 shell mapping contract\n");
meow_theme_export(&e1);
shell_theme sh = export_shell(&e1);
CHECK(sh.surface == e1.surface, "shell.surface == surface");
CHECK(sh.surface_variant == e1.surfaceVariant, "shell.surface_variant == surfaceVariant");
CHECK(sh.surface_container == e1.surfaceContainer, "shell.surface_container == surfaceContainer");
CHECK(sh.primary == e1.primary, "shell.primary == primary");
CHECK(sh.on_surface == e1.onSurface, "shell.on_surface == onSurface");
CHECK(sh.on_surface_variant == e1.onSurfaceVariant, "shell.on_surface_variant == onSurfaceVariant");
CHECK(sh.outline == e1.outline, "shell.outline == outline");
CHECK(sh.error == e1.error, "shell.error == error");
CHECK(sh.surface_tint == e1.surfaceTint, "shell.surface_tint == surfaceTint");
/* 关键角色字段值与 MIUIx light 内建一致(证明读的是 SDK 单一权威,而非 launcher 自持 4 套) */
CHECK(sh.surface == 0xF7F3EE && sh.primary == 0xF57200 && sh.error == 0xB3261E,
"shell roles match SDK builtin (single authority)");
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+152
View File
@@ -0,0 +1,152 @@
/* test_meow_ui_view_event.c — M7.0a 缺口#7 宿主验证:meow_view 事件接线 + 递归 enter/leave。
*
* A 部(无 run 循环,直调 meow_view_*):递归 enter/leave 顺序 + 运行期动态挂子视图立即进入
* (view.c meow_view_add_child 修正)+ 子>父 on_event 路由。
* B 部(经 meow_ui_run 主循环):把语义事件注入队列 → run 循环先经 meow_view_dispatch_event
* 路由,命中的事件不再落 App on_* 回调(on_tap/on_key/on_swipe/long_press 零触发),未命中
* 的事件照常回落 App 回调——验证「接线于 meow_ui_run、于 §6.4 分流之上」。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_ui_view_event test_meow_ui_view_event.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_view_event
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* MEOW_EV_* */
#include "meow_ui_view.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* ---------- A 部:递归 enter/leave 顺序 + 运行期动态挂子 ---------- */
static int g_trace[16]; static int g_trace_n = 0;
static void vtrace_enter(meow_view *v, void *ctx) { (void)ctx; g_trace[g_trace_n++] = v->id; }
static void vtrace_leave(meow_view *v, void *ctx) { (void)ctx; g_trace[g_trace_n++] = -v->id; }
static int g_child3_hits = 0;
static int vev_child3(meow_view *v, int ev, int a, int b, void *ctx)
{ (void)v;(void)ctx; if (ev == MEOW_EV_TAP && a == 33 && b == 33) { g_child3_hits++; return 1; } return 0; }
/* ---------- B 部:run 循环接线 ---------- */
static int g_tap_calls = 0, g_key_calls = 0, g_swipe_calls = 0, g_lp_calls = 0;
static void app_on_tap(int x, int y) { (void)x;(void)y; g_tap_calls++; }
static void app_on_key(int k, int d) { (void)k;(void)d; g_key_calls++; }
static void app_on_swipe(int dir, int dx, int dy) { (void)dir;(void)dx;(void)dy; g_swipe_calls++; }
static void app_lp(int x, int y) { (void)x;(void)y; g_lp_calls++; }
/* 父 view:消费 TAP@(10,10) 与 LONG_PRESS(任何)。 */
static int vev_parent(meow_view *v, int ev, int a, int b, void *ctx)
{ (void)v;(void)ctx;
if (ev == MEOW_EV_TAP && a == 10 && b == 10) return 1;
if (ev == MEOW_EV_LONG_PRESS) return 1;
return 0; }
/* 子 view:消费 KEY(任何)与 SWIPE_LEFT。 */
static int vev_child(meow_view *v, int ev, int a, int b, void *ctx)
{ (void)v;(void)b;(void)ctx;
if (ev == MEOW_EV_KEY) return 1;
if (ev == MEOW_EV_SWIPE && a == MEOW_SWIPE_LEFT) return 1;
return 0; }
static void *driver(void *arg)
{
(void)arg;
usleep(700000); /* 等 startup cooldown(500ms) 过后再发 */
meow_input_inject(MEOW_EV_KEY, MEOW_KEY_VOLUMEUP, 1); /* child 消费 → app_on_key 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_TAP, 10, 10); /* parent 消费 → app_on_tap 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_TAP, 50, 50); /* 无 view 命中 → 回落 app_on_tap */
usleep(80000);
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80); /* child 消费 → app_on_swipe 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_RIGHT, 80); /* 无 view 命中 → 回落 app_on_swipe */
usleep(80000);
meow_input_inject(MEOW_EV_LONG_PRESS, 10, 10); /* parent 消费 → long_press 不触发 */
usleep(80000);
meow_ui_request_exit();
return NULL;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/view_event.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
/* ================= A 部:递归 enter/leave + 动态挂子 ================= */
printf("A: recursive enter/leave + runtime child add\n");
{
static const meow_view_hooks ph = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
.on_event = vev_parent };
static const meow_view_hooks ch = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
.on_event = vev_child };
static const meow_view_hooks c3 = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
.on_event = vev_child3 };
meow_view *pv = meow_view_create(1, MEOW_VIEW_ACTIVITY, &ph, NULL);
meow_view *cv = meow_view_create(2, MEOW_VIEW_CONTAINER, &ch, NULL);
assert(meow_view_add_child(pv, cv) == 0);
g_trace_n = 0;
meow_view_page_push(1); /* pv.enter → cv.enter */
CHECK(g_trace_n == 2 && g_trace[0] == 1 && g_trace[1] == 2, "push: parent.enter -> child.enter");
/* 运行期动态挂第三个子视图(父已 ENTERED)→ 立即进入(view.c 修正) */
meow_view *c3v = meow_view_create(3, MEOW_VIEW_CONTAINER, &c3, NULL);
g_trace_n = 0;
assert(meow_view_add_child(pv, c3v) == 0);
CHECK(g_trace_n == 1 && g_trace[0] == 3, "runtime add-child-while-entered -> child.enter fires");
/* 新挂子 view 参与事件路由:命中 (33,33) 被子 3 消费 */
g_child3_hits = 0;
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 33, 33) == 1 && g_child3_hits == 1,
"runtime-added child participates in dispatch");
/* 离开:子视图(挂载序 2 → 3)先后离开 → 父(递归离开,子->父) */
g_trace_n = 0;
meow_view_page_pop();
CHECK(g_trace_n == 3 && g_trace[0] == -2 && g_trace[1] == -3 && g_trace[2] == -1,
"pop: child2.leave -> child3.leave -> parent.leave");
meow_view_free(pv); /* 递归释放 2/3 */
printf(" PASS A: recursive order + dynamic child\n");
}
/* ================= B 部:meow_ui_run 接线(§6.4 之上) ================= */
printf("B: meow_view_dispatch_event wired into meow_ui_run\n");
{
static const meow_view_hooks ph = { .on_event = vev_parent };
static const meow_view_hooks ch = { .on_event = vev_child };
meow_view *pv = meow_view_create(20, MEOW_VIEW_ACTIVITY, &ph, NULL);
meow_view *cv = meow_view_create(21, MEOW_VIEW_CONTAINER, &ch, NULL);
assert(meow_view_add_child(pv, cv) == 0);
meow_view_page_push(20); /* 进入父+子(g_top_view=20) */
g_tap_calls = g_key_calls = g_swipe_calls = g_lp_calls = 0;
meow_ui_set_long_press_cb(app_lp);
meow_ui_events ev; memset(&ev, 0, sizeof ev);
ev.on_tap = app_on_tap; ev.on_key = app_on_key; ev.on_swipe = app_on_swipe;
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
meow_ui_run(&ev); /* 阻塞直到 driver 里 request_exit */
pthread_join(thd, NULL);
CHECK(g_key_calls == 0, "KEY consumed by child -> app_on_key NOT called");
CHECK(g_tap_calls == 1, "TAP(10,10) consumed by parent; TAP(50,50) fell through -> exactly 1 on_tap");
CHECK(g_swipe_calls == 1, "SWIPE_LEFT consumed by child; SWIPE_RIGHT fell through -> exactly 1 on_swipe");
CHECK(g_lp_calls == 0, "LONG_PRESS consumed by parent -> long_press cb NOT called");
printf(" PASS B: run loop routes through views, fallback preserved\n");
meow_view_page_pop();
meow_view_free(pv);
}
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+1 -1
View File
@@ -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"
+74 -3
View File
@@ -4,9 +4,10 @@
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_img.c meow_ui_media.c \
* meow_notify_ipc.c stb_image.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕)
*
* 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm。
@@ -201,6 +202,75 @@ int main(void)
assert(meow_topappbar_hit(tab_, 440, 28) == 1); /* 右缘动作 "Edit"(最右) */
printf("PASS topappbar: create/back/action/hit\n");
/* ---------- TopAppBar 位图动作图标(sb_icons[],Material Symbols)----------
* 文本项默认;set_item_icon 把动作项改为位图 → 绘制用 meow_gfx_blit_icon_ink(32px 槽
* 居中 + ink 垂直对齐栏中点),颜色取主题 onSurface(按压向栏底色淡 12%)。断言用
* back_buf 真实像素:画完读亮度,非空 ink + 按压变淡 + 图标项命中。 */
{
/* 用独立 topbar(y=300,避让其它控件);meow_widget_draw 自填 surfaceContainerHigh 底 */
meow_widget *ti = W(meow_topappbar_create(0, 300, 480, 56, ""));
int ai = meow_topappbar_add_action(ti, ""); /* 文本占位 → 转图标 */
assert(ai == 0);
assert(meow_topappbar_set_item_icon(ti, ai, MEOW_ICON_SETTINGS) == 0);
assert(meow_topappbar_set_item_icon(ti, 99, MEOW_ICON_SETTINGS) == -1); /* 越界 */
assert(meow_topappbar_set_item_icon(ti, ai, -1) == -1); /* 非法图标 */
assert(meow_topappbar_set_item_icon(NULL, ai, MEOW_ICON_SETTINGS) == -1);
meow_widget_draw(ti);
/* 右起动作格(SDK 位图动作项宽 48)命中:几何中心 → 项 0(最右图标) */
assert(meow_topappbar_hit(ti, 444, 328) == ai);
/* 图标 ink 采样:与栏底色有「绝对对比度」的像素即 ink(日/夜主题都适用——
* 日 = 深 onSurface 画浅底、夜 = 浅 onSurface 画深底,对比度都大)。 */
int x0 = 480 - 48, y0 = 300 + 10, x1 = 480, y1 = 300 + 46;
int bg_b, bg_g, bg_bgv;
rd_px(480 - 3, 302, &bg_b, &bg_g, &bg_bgv); /* 栏右上角 ≈ 背景 */
int bg_lum = (bg_b + bg_g + bg_bgv) / 3;
int sum_ink = 0, cnt_ink = 0;
for (int xx = x0; xx < x1; xx++) {
for (int yy = y0; yy < y1; yy++) {
int rr, gg, bb;
rd_px(xx, yy, &rr, &gg, &bb);
int lum = (rr + gg + bb) / 3;
int d = lum - bg_lum; if (d < 0) d = -d;
if (d > 20) { sum_ink += d; cnt_ink++; }
}
}
assert(cnt_ink > 4); /* 位图 ink 非空(为抗锯齿留余量) */
int before = sum_ink;
/* 按压态 → 图标向栏底色淡 12% → 对比度整体下降 */
meow_widget_set_state(ti, MEOW_WS_PRESSED, 1);
meow_widget_draw(ti);
sum_ink = 0;
for (int xx = x0; xx < x1; xx++) {
for (int yy = y0; yy < y1; yy++) {
int rr, gg, bb;
rd_px(xx, yy, &rr, &gg, &bb);
int lum = (rr + gg + bb) / 3;
int d = lum - bg_lum; if (d < 0) d = -d;
if (d > 20) sum_ink += d;
}
}
assert(sum_ink < before); /* 按压变淡 */
meow_widget_set_state(ti, MEOW_WS_PRESSED, 0);
printf("PASS topappbar: bitmap action icon draw/hit/pressed-fade (ink=%d->%d)\n",
before, sum_ink);
}
/* ---------- 状态栏图标 blit 路径回归(meow_gfx_blit_icon 同源核心)----------
* 证明 refactor 后状态栏路径(blit + 顶栏脏区)仍写非空像素;复用 SB_ICON_WIFI(0)。 */
{
meow_ui_fill_rect(0, 0, 480, 28, 0x000000); /* 清顶栏区为黑底 */
int bgr, bgg, bgb, bright = 0;
meow_gfx_blit_icon(0, 0, 0, 16, 0xFFFFFF, 14); /* idx0=WIFI 槽左缘 0 */
for (int xx = 0; xx < 32; xx++) {
for (int yy = 4; yy < 24; yy++) {
rd_px(xx, yy, &bgr, &bgg, &bgb);
if ((bgr + bgg + bgb) / 3 > 40) bright++;
}
}
assert(bright > 4); /* 状态栏 WiFi 位图 ink 非空(黑底 + 白前景 → 亮像素) */
printf("PASS statusbar: meow_gfx_blit_icon(0) draws non-empty ink (brt=%d)\n", bright);
}
/* ---------- NavigationBar ---------- */
meow_widget *nav = W(meow_navigationbar_create(0, 680, 480, 40));
int n0 = meow_navigationbar_add_item(nav, "Home");
@@ -249,6 +319,7 @@ int main(void)
* lv 位于 (20,240,320,112)item_h=56 → 行0 y240..296、行1 296..352。
* 断言全部「画完读 back_buf 像素」(region_has/px_near),非目检。 */
meow_widget *lv = W(meow_listview_create(20, 240, 320, 112, 4, lv_src, NULL, MEOW_LIST_SEL_SINGLE));
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,此测试专测 set_container(0) 扁平回退路径 */
assert(meow_listview_set_item_h(lv, 56) == 56);
const meow_theme *lth = meow_theme_get();
int lvx = 20, lvy = 240, lvw = 320, lvh = 112, itemh = 56;
+28
View File
@@ -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;
}
+3 -2
View File
@@ -16,8 +16,9 @@
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/ime/media/notify_ipc + 打桩 font/global):
* gcc -Wall -Wextra -o test_notif_gesture test_notif_gesture.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_media.c meow_notify_ipc.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_str.c meow_ui_activity.c meow_ui_media.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_notify_ipc.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_notif_gesture
*/
#include "meow_ui.h"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -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
View File
@@ -1,21 +1,27 @@
/* test_overlay_suppress.c — §10.5 叠加让位 present 自抑宿主验证(离屏 480x720,16bpp)。
*
* 背景:媒体播放中的 App 前台唤起通知中心时 launcher 不 SIGSTOP(保 PCM 供流),改由 SDK
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏。
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏;
* 批①后台常驻后标记**内容化**:内容=当前前台 app_id(空=菜单/叠加=全抑制)。本测试同时验证
* 批①多 App 常驻的「前台/后台」判定:内容==本进程 app_id(env MEOW_LOG_APP)→ 前台不抑制;
* 内容为其它 app_id / 空 → 后台常驻/被覆盖 → 自抑。
*
* 覆盖:
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
* T2 有标记:flush 被抑制(只画进 back_buf 不 present:frames 不变、抑制计数递增);
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
* T3 标记消失:下一次 flush 恢复上屏(抑制期累积脏区一次上屏,无需重画);
* T4 系统 UI 进程(launcher,system_mode=1)即使有标记也不抑制(面板必须照常上屏)。
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
* T2 标记=其它 app_id(后台常驻)→ 抑制(只画进 back_buf 不上屏:frames 不变、计数递增);
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
* T2c 标记=自身 app_id(前台 App)→ 不抑制(恢复上屏,恢复边沿整帧重画;计数不再增长);
* T3 标记=空内容(菜单/叠加全抑制)→ 仍抑制;
* T3b 标记消失 → 恢复上屏(抑制期累积脏区一次上屏,无需重画);
* T4 系统 UI 进程(launcher,system_mode=1)即使有标记也不抑制(面板必须照常上屏)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_overlay_suppress test_overlay_suppress.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_media.c \
* meow_notify_ipc.c meow_ui_nav.c \
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
@@ -26,13 +32,16 @@
#include <unistd.h>
#include <fcntl.h>
#define MARKER "/tmp/test_notif_active_sdk"
#define MARKER "/tmp/test_notif_active_sdk"
#define SELF_ID "testapp"
static void marker_write(void)
static void marker_write(const char *content)
{
int fd = open(MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
assert(fd >= 0);
if (write(fd, "1", 1) < 0) assert(0);
if (content && content[0]) {
if (write(fd, content, strlen(content)) < 0) assert(0);
}
close(fd);
}
@@ -40,6 +49,7 @@ int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/overlay_suppress.ppm", 1);
setenv("MEOW_NOTIF_ACTIVE_FILE", MARKER, 1);
setenv("MEOW_LOG_APP", SELF_ID, 1); /* 本测试进程身份=前台/后台的判定锚 */
unlink(MARKER);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
@@ -53,15 +63,15 @@ int main(void)
assert(meow_gfx_overlay_suppressed_count() == 0);
printf("T1 PASS: no marker -> presented=1 suppressed=0\n");
/* T2:有标记 → 抑制(等 >200ms 越过存在性节流缓存) */
marker_write();
/* T2:标记=其它 app_id(后台常驻 App)→ 抑制(等 >200ms 越过存在性节流缓存) */
marker_write("ncm");
usleep(250000);
unsigned long long f0 = meow_ui_frames_presented();
meow_ui_clear(0x223344);
meow_ui_flush();
assert(meow_ui_frames_presented() == f0);
assert(meow_gfx_overlay_suppressed_count() == 1);
printf("T2 PASS: marker present -> suppressed (frames frozen at %llu)\n", f0);
printf("T2 PASS: marker=other app -> suppressed (frames frozen at %llu)\n", f0);
/* T2b:抑制期再次 flush 仍抑制(节流缓存内一致) */
meow_ui_clear(0x334455);
@@ -70,24 +80,43 @@ int main(void)
assert(meow_gfx_overlay_suppressed_count() == 2);
printf("T2b PASS: still suppressed within throttle window\n");
/* T3:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏,无需重画) */
/* T2c:标记=自身 app_id(前台 App)→ 不抑制(批①前后台判定核心;恢复边沿整帧重画一次上屏) */
marker_write(SELF_ID);
usleep(250000);
meow_ui_clear(0x445566);
meow_ui_flush();
assert(meow_ui_frames_presented() == f0 + 1);
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长(前台不抑制) */
printf("T2c PASS: marker=self -> foreground, present resumed (accumulated dirty flushed)\n");
/* T3:标记=空内容(菜单/叠加层覆盖=全抑制)→ 仍抑制 */
marker_write("");
usleep(250000);
unsigned long long f1 = meow_ui_frames_presented();
meow_ui_clear(0x556677);
meow_ui_flush();
assert(meow_ui_frames_presented() == f1);
assert(meow_gfx_overlay_suppressed_count() == 3);
printf("T3 PASS: empty marker (menu/overlay) -> suppressed\n");
/* T3b:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏) */
unlink(MARKER);
usleep(250000);
meow_ui_flush();
assert(meow_ui_frames_presented() == f0 + 1);
assert(meow_gfx_overlay_suppressed_count() == 2);
printf("T3 PASS: marker gone -> present resumed (accumulated dirty flushed)\n");
assert(meow_ui_frames_presented() == f1 + 1);
assert(meow_gfx_overlay_suppressed_count() == 3);
printf("T3b PASS: marker gone -> present resumed (accumulated dirty flushed)\n");
/* T4:系统 UI 进程(launcher)不抑制——通知中心面板本身必须照常上屏 */
meow_ui_sys_set_system_mode(1);
assert(meow_ui_sys_render_enabled() == 1);
marker_write();
marker_write("ncm");
usleep(250000);
unsigned long long f1 = meow_ui_frames_presented();
meow_ui_clear(0x445566);
unsigned long long f2 = meow_ui_frames_presented();
meow_ui_clear(0x667788);
meow_ui_flush();
assert(meow_ui_frames_presented() == f1 + 1);
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长 */
assert(meow_ui_frames_presented() == f2 + 1);
assert(meow_gfx_overlay_suppressed_count() == 3); /* 不再增长 */
unlink(MARKER);
printf("T4 PASS: system-UI process (launcher) never suppressed\n");
+6 -5
View File
@@ -15,15 +15,16 @@
* P2 list 焦点:命中非 list 区域时释放列表焦点、切页滚动(不再沿用旧焦点继续滚列表)。
* P3 list 脏区:W_LIST 绘制已裁剪到视口;滚动(非整行偏移)+选中行不再向标题间隙/底缘下泄漏。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime/img/config/
* notify/ipc + 打桩 font/global(test_host_stub) + stb_image + hello 拆分层):
* 编译(需主仓组件,公开仓跳过):本仓不含 ../hello_app(公开仓为 examples/hello),且本文件
* 直接 #include "../hello_app/hello_internal.h";以下路径按主仓布局(打桩 font/global +
* stb_image + hello 拆分层):
* 1) 单独编译 app.c 并把 main 改名 hello_app_main(否则与本文件 main 冲突):
* gcc -c -Wall -Wextra -DMEOW_UI_HOST_TEST -Dmain=hello_app_main \
* -I. -I../hello_app ../hello_app/app.c -o app.o
* -I. -Iinclude -I../hello_app ../hello_app/app.c -o app.o
* 2) 编译本文件 + 其余 hello 模块 + SDK 源 + 打桩,链接 app.o:
* gcc -Wall -Wextra -DMEOW_UI_HOST_TEST -o test_p1p2p3_repro test_p1p2p3_repro.c app.o \
* test_host_stub.c meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_ui_img.c meow_ui_config.c meow_ui_notify.c meow_notify_ipc.c meow_ui_draw.c \
* meow_ui_view.c meow_ui_nav.c meow_ui_media.c stb_image.c \
* ../hello_app/ui/home/home.c ../hello_app/ui/full.c ../hello_app/ui/demo/demo.c \
@@ -36,7 +37,7 @@
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "../examples/hello/hello_internal.h"
#include "../hello_app/hello_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
+3 -3
View File
@@ -9,9 +9,9 @@
* 编译(宿主 gcc,同 test_ime_fix):
* gcc -Wall -Wextra -o test_pinyin test_pinyin.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_ui_media.c \
* meow_notify_ipc.c stb_image.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_img.c meow_ui_media.c \
* meow_notify_ipc.c stb_image.c meow_ui_nav.c -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
+1 -1
View File
@@ -10,7 +10,7 @@
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log + 打桩 font/global + meow_notify_ipc):
* gcc -o test_sb_collapse test_sb_collapse.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c meow_notify_ipc.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more