diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md index b78ca6c..11a7f50 100644 --- a/MEOW_APP_SDK.md +++ b/MEOW_APP_SDK.md @@ -326,6 +326,8 @@ patch_v2/meow_launcherd/meow_font.a # 复用(静态链接) > **app 字体来源(M2.x 已落地)**:app 若**自带字库**(`load_handle`,见 §5.7.3,**未实现**)则用它;否则 SDK `meow_ui_gfx.c load_font()` **优先读 launcher 注入的环境变量** `MEOW_FONT_PNG`/`MEOW_FONT_IDX`(=系统 `font.file` 当前生效字体,绝对路径)/`MEOW_FONT_APP`(family),无 env 则回退卡默认 `MeowOS/fonts/default/font.png` → 固件。因此**app 未自带字库时跟随系统 `font.file`**,不再硬编码 `MeowAPPs/fonts`。 > > **app 多字体(M2.5x,`display.multi_font`)**:launcher 注入 `MEOW_MULTI_FONT=0/1` 与 6 类 `MEOW_FONT_CLS_<类>_PNG/IDX`(命中则覆盖全局;类值为空=跟随全局不注入)。SDK `meow_ui_gfx` 的 `meow_gfx_load_multifont()` 读 env → 调 `meow_font_config_multifont()`(同 png 去重、≤4 套上限、缺类回退全局)构建**逐字符分类路由**:`meow_font_drawtext/width_text/line_height` 按 `meow_font_class_of(cp)`(`MEOW_FCLS_CJK/_CJK_SYM/_NUM/_LATIN/_LATIN_SYM/_OTHER`)用对应字库取字形与度量;`meow_font_has_glyph(cp)` 可预判缺字。`display.multi_font=0`/无 env → 单字体(现状,零额外开销)。底层接口见 meow_font.h:`meow_font_load_slot`/`meow_font_unload_slot`/`meow_font_enable_multifont`/`meow_font_set_class_slot`/`meow_font_config_multifont`;`MEOW_MAX_SLOTS=5`。 +> +> **app 字体档位(M2.5x,`app.json "fonts"` 声明 + `font.load_full`)**:launcher 按 `app.json "fonts":"common|gbk|full"` 与系统「加载大字库」开关(`font.load_full`,默认 0;开=强制 ≥gbk 档)算出**生效档**,注入 `MEOW_FONT_TIER=<0|1|2>`(0=**common**≈4k 常用 1=**gbk**≈20k+ 大 2=**full** 全/可选),并把 `MEOW_FONT_PNG/IDX` 指向该档字库绝对路径——`meow_font_effective_paths_tiered()` 在卡内 `MeowOS/fonts//` 按 `font.json` 的 `charset`(`common|gbk|full`)选库(gbk 请求接受 gbk/full;full 请求优先 full 再降级 gbk;无匹配回退全局 `font.file`)。SDK `meow_ui_gfx.c load_font()` 载入后,应用可经 `meow_font_tier()` 查询当前档、`meow_font_set_tier()` 切档(触发 `meow_font_reload_tier()` **真重载**:大字库切换先 unload 旧再 load 新,释放旧 atlas,防 GBK/Full 大库双驻留)。**判缺字**:`meow_font_has_glyph(cp)` 预判当前字库是否含该码位(common 小库下缺字→app 回退/提示,避免静默豆腐)。底层接口见 meow_font.h `meow_font_reload_tier`;`font/gen/gen_font.py --charset common|gbk|full` 按档生成 `font.png/font.idx/font.json`(`font.json` 写 `charset`+`glyph_count`)。 --- diff --git a/docs/MEOWOS_SPEC_REF.md b/docs/MEOWOS_SPEC_REF.md index ac932c8..6329186 100644 --- a/docs/MEOWOS_SPEC_REF.md +++ b/docs/MEOWOS_SPEC_REF.md @@ -161,9 +161,9 @@ - **preset 动态化**:启动/进设置页扫描一次 `MeowOS/fonts/*/`,重建「字体文件」下拉(默认字体 + ≤16 套卡内字体;上限防爆)。扫描仅此二时机,勿放热路径。 - **MeowPanel 预留**:`meow_font_import_zip()`/`meow_font_export_zip()` 为 TODO 声明(不实现),供 MeowPanel 后续接入(`.zip` 导入校验→解压到 `MeowOS/fonts//`、导出打包)。 - **多字体(M2.5x,`display.multi_font`)**:按字符类别(CJK/CJK_SYM/NUM/LATIN/LATIN_SYM/OTHER)逐字符路由,每类可各选一套字库(`font.class_*`,默认空=跟随全局 `font.file`);同 family 去重、同时装载不同字体 ≤4 套(超出 WARN「过多不同字体会增内存」并截断);开关关 = 单字体(现状)。详见 §5.7.7「多字体」。 - - **字体档位 / 大字库(M2.5x)**:抽象出**字库大小档位**,让系统/应用/设置页都知道选的是多大字库 `[源:用户 2026-08-30]`。 + - **字体档位 / 大字库(M2.5x,本次实现)**:抽象出**字库大小档位**,让系统/应用/设置页都知道选的是多大字库 `[源:用户 2026-08-30]`。 - 档位枚举:`MEOW_FONT_TIER_COMMON`(**默认 / ≈4k** 码位)/ `MEOW_FONT_TIER_GBK`(**大 / ≈20k+**)/ `MEOW_FONT_TIER_FULL`(**全 / 可选**,码位/体积最大)。 - - `font/gen/gen_font.py` 增 `--charset common|gbk|full` 按档生成 `font.png/font.idx`(现行仅 `--extra` 思路升级为**档位选择**,**本次新增**);每个 `MeowOS/fonts//font.json` 标 `charset` + `glyph_count`(覆盖声明/信息用)。 + - `font/gen/gen_font.py` `--charset common|gbk|full` 按档生成 `font.png/font.idx/font.json`(**已实现**,现行仅 `--extra` 思路升级为**档位选择**);每个 `MeowOS/fonts//font.json` 标 `charset` + `glyph_count`(覆盖声明/信息用,launcher/settings 据此标注与按需装载)。 - **默认用小字库(`common`≈4k)**;仅当 app 声明需大字库(`app.json "fonts":"full"` 或档位)才为该 app 加载大字库(`gbk`≈20k+),见 §5.7.8。 - **系统设置「加载大字库」开关**(`font.load_full`,强制全系统用大字库),见 SYSTEM_SETTINGS §1;**每个字体选项标注字库大小标记**(如 `default(common≈4k)` / `gbk≈20k` / `full`),让用户知道所选字库量级。 - 判缺字:SDK `meow_font_has_glyph(cp)` 预判当前字库是否含该码位(缺字 → app 回退/提示)。 @@ -296,9 +296,9 @@ - **launcher env 注入**(launch_app,app 跟随系统字体):`MEOW_MULTI_FONT=0/1`;`MEOW_FONT_PNG/IDX`(全局,保留);6 类 `MEOW_FONT_CLS_<类>_PNG/IDX`(命中则覆盖全局;类值为空=跟随全局不注入)。SDK `meow_ui_gfx` 读 env 构建同一映射;无 env/关 → 单字体。 - **持久化**:类别字体设置在 settings.conf(板载,无卡也可存),经 `meow_settings_*` 读写,随设置页变更即重配。 - **内存**(推断解码 atlas = `plane_w×plane_h×(depth 4→0.5 / 8→1.0)` 字节;每套另加 glyph 数组 ≈ glyph_count×16B、idx 头保留约 128KB,idx 原始缓冲载毕即释)。实测仓库字库(depth 4):misans(CJK)≈2.44MiB、source_han_sans(CJK)≈2.53MiB、meslo(Latin)≈1.68MiB、google_sans(Latin)≈1.12MiB;按用户估算口径(**depth 8**):CJK ≈ 5-6MB、Latin ≈ 0.3-0.5MB(后者为窄覆盖 Latin 专库)。**典型「1CJK+1Latin」增量**:全局=CJK,另加一套 Latin → 增加 ≈0.3-1.7MB(depth 8 约 0.5MB;实测 depth4 latin 1.1-1.7MB);全局=Latin 另加一套 CJK → 增加 ≈2.4-2.5MB(depth 8 约 5-6MB)。`display.multi_font=0` 时仅全局一套,无任何额外装载。 -- **5.7.8 字体档位分层 / 按需大字库(app 声明 + 系统开关)** `[源:用户 2026-08-30]` +- **5.7.8 字体档位分层 / 按需大字库(app 声明 + 系统开关,本次实现)** `[源:用户 2026-08-30]` - **口径**:默认用**小字库 `common`(≈4k 码位)**;当 app 声明需要**大字库**(如小说阅读器,`app.json` 声明 `"fonts":"full"` 或档位 `"gbk"`/`"common"`)时,才为该 app 加载**大字库 `gbk`(≈20k+)**。档位与 `gen_font.py --charset`、`font.json charset/glyph_count` 见 §5.2「字体档位/大字库」。 - - **app 级 / activity 级**:app 级以 `app.json "fonts":""` 声明;activity 级(如需)以 `route..fonts` 声明(沿用 `route.` 扩展口径)。launcher 启动 app/切 activity 时据声明选加载大/小字库,并经 env 注入(`MEOW_FONT_PNG/IDX` 指向所选档位字库;新增 `MEOW_FONT_TIER` 标注档位)。 + - **app 级 / activity 级**:app 级以 `app.json "fonts":""` 声明(launcher `meow_parse_app_json` 已解析);activity 级(如需)以 `route..fonts` 声明(沿用 `route.` 扩展口径)。launcher 启动 app 时据声明 + `font.load_full` 算**生效档**,经 env 注入(`MEOW_FONT_PNG/IDX` 指向所选档位字库绝对路径;`MEOW_FONT_TIER` 标注档位;launcher `meow_font_effective_paths_tiered` 按卡内 `font.json charset` 选库)。 - **系统设置「加载大字库」**(`font.load_full`,默认 0):开 → **强制全系统用大字库(gbk 档)**,覆盖 app 未声明 + 覆盖默认 `font.file`;关(默认)→ 默认小字库,app 声明才大字库。 - **SDK 判缺字**:`meow_font_has_glyph(cp)` 预判当前字库是否含该码位 → app 据此决定回退/提示(避免 `common` 小字库下静默豆腐或缺字)。 - **⚠️ 与 §5.7.3 冲突**:§5.7.3 `"fonts":[…]` 为**应用自带字库句柄数组**;本小节 `"fonts":"full"` 为**字库档位字符串**。同名键两种语义,需统一解析口径(见 §7 A-14)。 diff --git a/font/gen/gen_font.py b/font/gen/gen_font.py index 91f1139..b3dd6cf 100644 --- a/font/gen/gen_font.py +++ b/font/gen/gen_font.py @@ -44,8 +44,11 @@ idx 二进制布局(little-endian, version=2): +15 u8 dy """ import argparse +import hashlib +import json import os import struct +import unicodedata import zlib from PIL import Image, ImageDraw, ImageFont @@ -96,32 +99,80 @@ def pack_plane_depth(plane_l8, width, height, depth): # -------------------------------------------------------------------------- -# 字符集 +# 字符集(字体档位:common / gbk / full) +# 口径见 docs/MEOWOS_SPEC_REF.md §5.2「字体档位/大字库」与 §5.7.8: +# common ≈ 4k 码位(ASCII + GB2312 常用 + 标点 + --extra) = 现状; +# gbk ≈ 20k+(再并入 CJK 统一表意 U+4E00-9FFF + CJK 符号/全角,含常用简繁/生僻); +# full ≈ 全 Unicode 可渲染区间(gbk + CJK 扩展 A/B-F + 拉丁/希腊/西里尔 + 通用标点,可选)。 +# 每档在产物 font.json 写入 charset + glyph_count(见 write_font_json)。 # -------------------------------------------------------------------------- -def build_charset(extra=""): +CHARSET_TIERS = ("common", "gbk", "full") +CHARSET_ALIAS = {"0": "common", "common": "common", "1": "gbk", "gbk": "gbk", + "2": "full", "full": "full"} + + +def build_charset(tier="common", extra=""): + if tier not in CHARSET_TIERS: + tier = "common" cps = [] def add(cp): - if cp not in cps: + if 0 <= cp <= 0x10FFFF and cp not in cps: cps.append(cp) + # ASCII 可打印字符 for c in range(0x20, 0x7F): add(c) + # GB2312 一/二级常用简体(B0-D7 + D8 常用二级首段) for hi in range(0xB0, 0xD8): for lo in range(0xA1, 0xFF): try: add(ord(bytes([hi, lo]).decode("gb2312"))) except UnicodeDecodeError: pass - for hi in range(0xA1, 0xA4): + # GB2312 符号/全角(A1-A4) + for hi in range(0xA1, 0xA5): for lo in range(0xA1, 0xFF): try: add(ord(bytes([hi, lo]).decode("gb2312"))) except UnicodeDecodeError: pass + if tier in ("gbk", "full"): + # CJK 统一表意(含常用简繁/生僻):U+4E00-9FFF ≈ 2万+ + for cp in range(0x4E00, 0xA000): + add(cp) + # CJK 符号与标点 + 全角形式(补齐 common 未含的) + for cp in range(0x3000, 0x3040): + add(cp) + for cp in range(0xFF00, 0xFFF0): + add(cp) + if tier == "full": + # 拉丁补充 / 希腊 / 西里尔 等常用非 CJK(Latin-1 起已覆盖 AI 常见重音) + for cp in range(0xA0, 0x0530): + add(cp) + # CJK 扩展 A + 兼容表意 + for cp in range(0x3400, 0x4DC0): + add(cp) + for cp in range(0xF900, 0xFB00): + add(cp) + # 通用标点 / 货币等(— … “ ” 等已在 gbk;此处补全) + for cp in range(0x2000, 0x2070): + add(cp) + for cp in range(0x20A0, 0x20D0): + add(cp) + # CJK 扩展 B-F(补充平面) + for cp in range(0x20000, 0x2EBF0): + add(cp) + for cp in range(0x30000, 0x31350): + add(cp) for ch in extra: add(ord(ch)) return sorted(cps) +def charset_glyph_count(cps): + """glyph_count = 字符集码位数(信息用,落 font.json)。""" + return len(cps) + + # -------------------------------------------------------------------------- # 逐字形渲染 # -------------------------------------------------------------------------- @@ -227,6 +278,33 @@ def write_idx(path, bit_depth, faces, glyph_tabs): return len(header) + len(body) +# -------------------------------------------------------------------------- +# font.json(元数据 + 完整性校验;schema 见 docs/MEOWOS_SPEC_REF.md §5.2) +# -------------------------------------------------------------------------- +def write_font_json(out_dir, name, family, tier, glyph_count, sizes, bitdepth, + png_path, idx_path): + def sha256(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for blk in iter(lambda: f.read(1 << 16), b""): + h.update(blk) + return h.hexdigest() + meta = { + "family": family or name, # 同目录名(信息用;扫描端以目录名为 family) + "name": name, # 下拉显示名(缺则回退目录名) + "glyph_count": glyph_count, # 信息用 + "charset": tier, # common|gbk|full(档位;驱动按需加载) + "size_list": sizes, # 原生面字号 + "bit_depth": bitdepth, # 4|8 + "sha256_png": sha256(png_path), # font.png 校验(扫描端仅 WARN) + "sha256_idx": sha256(idx_path), # font.idx 校验(扫描端仅 WARN) + } + jpath = os.path.join(out_dir, "font.json") + with open(jpath, "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + return jpath + + # -------------------------------------------------------------------------- # 主流程 # -------------------------------------------------------------------------- @@ -236,14 +314,18 @@ def main(): ap.add_argument("--sizes", default="28,20") ap.add_argument("--out", required=True) ap.add_argument("--name", default="font") + ap.add_argument("--family", default="", help="font.json family(缺省 = --name;信息用)") + ap.add_argument("--charset", default="common", choices=CHARSET_TIERS, + help="字体档位:common≈4k / gbk≈20k+ / full(可选,默认不推)") ap.add_argument("--bitdepth", type=int, default=4, choices=(4, 8)) ap.add_argument("--extra", default="") args = ap.parse_args() os.makedirs(args.out, exist_ok=True) sizes = [int(s) for s in args.sizes.split(",") if s.strip()] - cps = build_charset(args.extra) - print(f"字符集: {len(cps)} 码位") + tier = CHARSET_ALIAS.get(args.charset, "common") + cps = build_charset(tier, args.extra) + print(f"字符集: {len(cps)} 码位 (charset={tier})") all_planes = [] # (aw, ah, plane_l8 bytes, tab) max_w = 0 @@ -311,10 +393,15 @@ def main(): idx_path = os.path.join(args.out, f"{args.name}.idx") idx_size = write_idx(idx_path, args.bitdepth, face_meta, [p[3] for p in all_planes]) + # ---- font.json(写入 charset + glyph_count + sha256,供扫描端标注档位/大小标记) ---- + jpath = write_font_json(args.out, args.name, args.family, tier, charset_glyph_count(cps), + sizes, args.bitdepth, png_path, idx_path) + png_size = os.path.getsize(png_path) total = png_size + idx_size print(f"\nfont.png {png_size} B") print(f"font.idx {idx_size} B") + print(f"font.json {os.path.getsize(jpath)} B (charset={tier}, glyph_count={len(cps)})") print(f"合计 {total} B = {total/1024:.1f} KiB = {total/1048576:.2f} MiB (预算 2MiB)") diff --git a/font/meow_font.c b/font/meow_font.c index b6b0f17..05f0a34 100644 --- a/font/meow_font.c +++ b/font/meow_font.c @@ -529,6 +529,22 @@ int meow_font_load(const char *png_path, const char *idx_path) { return meow_font_load_slot(0, png_path, idx_path); } +/* ---------------- 按档装载大字库(字体档位分层) ---------------- */ +/* 当前装载档位(0=common 1=gbk 2=full;仅记录,getter 走 meow_ui_gfx.c meow_font_tier) */ +static int g_reload_tier = 0; + +int meow_font_reload_tier(int tier, const char *png_path, const char *idx_path) +{ + if (tier < 0) tier = 0; + if (tier > 2) tier = 2; + if (!png_path || !png_path[0] || !idx_path || !idx_path[0]) return -1; + /* 先 unload 旧(释放旧 atlas——大字库 GBK/Full 解码 5-15MB+,避免新旧同时驻留内存), + * 再 load 新(slot0 = 全局/默认字库)。meow_font_load 内部即 unload+load。 */ + int r = meow_font_load(png_path, idx_path); + if (r == 0) g_reload_tier = tier; + return r; +} + void meow_font_enable_multifont(int on) { g_multifont = on ? 1 : 0; } void meow_font_set_class_slot(meow_fclass cls, int slot) { diff --git a/font/meow_font.h b/font/meow_font.h index 9ceb4bd..11139ce 100644 --- a/font/meow_font.h +++ b/font/meow_font.h @@ -33,6 +33,15 @@ int meow_font_load(const char *png_path, const char *idx_path); /* 释放字库。 */ void meow_font_unload(void); +/* 按档装载大字库(字体档位分层,见 MEOWOS_SPEC_REF §5.2/§5.7.8 与 meow_ui.h §10): + * tier = 0 common≈4k(小/默认)/ 1 gbk≈20k+(大字库)/ 2 full(全,可选)。 + * png/idx 为调用方已按档解析的设备绝对路径(launcher 经 env 注入)。切换时先 unload 旧 + * 再 load 新(释放旧 atlas —— 大字库 GBK 解码约 5-15MB,避免新旧同时驻留内存)。 + * 返回 0 成功,负值失败(失败时旧库已释放,调用方应回退卡默认/固件)。 + * 由 meow_ui_gfx.c 的 meow_font_set_tier() 在档位变化时唤起;meow_font_load() 仍是装载 + * 系统默认(slot0)字库的入口。 */ +int meow_font_reload_tier(int tier, const char *png, const char *idx); + /* 某字号是否存在(命中原生面返回 1)。 */ int meow_font_has_size(int size); diff --git a/font/test_multi_font.c b/font/test_multi_font.c index dd48e56..9cbd80a 100644 --- a/font/test_multi_font.c +++ b/font/test_multi_font.c @@ -266,6 +266,38 @@ int main(void) { CHECK(distinct == MEOW_MAX_SLOTS, "distinct capped at MEOW_MAX_SLOTS"); } + /* ---- (3) 字体档位分层:meow_font_reload_tier(大字库按档装载/切换)---- + * 用同两套 stub(lat=common 模拟,cjk=gbk 模拟)+ 一个不会释放的 cjk 大面,验证 + * tier>0 装载大字库、切换 unload 旧/load 新、失败回退。 */ + printf("\n== 字体档位分层 meow_font_reload_tier ==\n"); + /* 档位 0=common/1=gbk/2=full(与 meow_ui.h MEOW_FONT_TIER_* 一致;本测试只链 meow_font.h) */ + r = meow_font_reload_tier(0, "/tmp/mf_lat.png", "/tmp/mf_lat.idx"); + CHECK(r == 0, "reload_tier(common, lat) ok"); + CHECK(meow_font_has_glyph('A') == 1, "reload common -> has_glyph(A)=1"); + CHECK(meow_font_has_glyph(0x4E2D) == 0, "reload common -> has_glyph(中)=0 (lat no cjk)"); + CHECK(meow_font_width_text("A", 20) == 10, "reload common -> w(A)=10"); + /* 切到大字库(cjk 槽):应 unload 旧 lat 再 load cjk → '中' 可用、'A' 缺。 */ + r = meow_font_reload_tier(1, "/tmp/mf_cjk.png", "/tmp/mf_cjk.idx"); + CHECK(r == 0, "reload_tier(gbk, cjk) ok"); + CHECK(meow_font_has_glyph(0x4E2D) == 1, "reload gbk -> has_glyph(中)=1"); + CHECK(meow_font_has_glyph('A') == 0, "reload gbk -> has_glyph(A)=0 (cjk lacks lat)"); + CHECK(meow_font_width_text("中", 20) == 30, "reload gbk -> w(中)=30 (cjk slot)"); + CHECK(meow_font_line_height(20) == 33, "reload gbk -> lh(20)=33 (cjk ascent25+desc8)"); + /* 切回 common:再 unload cjk / load lat → 'A' 回来。 */ + r = meow_font_reload_tier(0, "/tmp/mf_lat.png", "/tmp/mf_lat.idx"); + CHECK(r == 0, "reload_tier(common) again ok"); + CHECK(meow_font_has_glyph('A') == 1, "reload back common -> has_glyph(A)=1"); + CHECK(meow_font_has_glyph(0x4E2D) == 0, "reload back common -> has_glyph(中)=0"); + CHECK(meow_font_width_text("A", 20) == 10, "reload back common -> w(A)=10"); + /* Full 档(tier=2)同一套 cjk 作为大字库装载也应成功(档仅记录,装载同路径)。 */ + r = meow_font_reload_tier(2, "/tmp/mf_cjk.png", "/tmp/mf_cjk.idx"); + CHECK(r == 0, "reload_tier(full, cjk) ok"); + CHECK(meow_font_has_glyph(0x4E2D) == 1, "reload full -> has_glyph(中)=1"); + /* 非法参数:NULL/空路径 → 负值(不崩),且字库已被上次 reload 释放(档位装载失败不驻留)。 */ + r = meow_font_reload_tier(1, NULL, NULL); + CHECK(r < 0, "reload_tier(NULL paths) -> negative"); + CHECK(meow_font_has_glyph('A') == 0, "reload failed -> font unloaded (has_glyph=0)"); + printf("\n%s (%d failures)\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS", g_fail); return g_fail ? 1 : 0; } diff --git a/patch_v2/meow_launcherd/meow_font.h b/patch_v2/meow_launcherd/meow_font.h index 9ceb4bd..11139ce 100644 --- a/patch_v2/meow_launcherd/meow_font.h +++ b/patch_v2/meow_launcherd/meow_font.h @@ -33,6 +33,15 @@ int meow_font_load(const char *png_path, const char *idx_path); /* 释放字库。 */ void meow_font_unload(void); +/* 按档装载大字库(字体档位分层,见 MEOWOS_SPEC_REF §5.2/§5.7.8 与 meow_ui.h §10): + * tier = 0 common≈4k(小/默认)/ 1 gbk≈20k+(大字库)/ 2 full(全,可选)。 + * png/idx 为调用方已按档解析的设备绝对路径(launcher 经 env 注入)。切换时先 unload 旧 + * 再 load 新(释放旧 atlas —— 大字库 GBK 解码约 5-15MB,避免新旧同时驻留内存)。 + * 返回 0 成功,负值失败(失败时旧库已释放,调用方应回退卡默认/固件)。 + * 由 meow_ui_gfx.c 的 meow_font_set_tier() 在档位变化时唤起;meow_font_load() 仍是装载 + * 系统默认(slot0)字库的入口。 */ +int meow_font_reload_tier(int tier, const char *png, const char *idx); + /* 某字号是否存在(命中原生面返回 1)。 */ int meow_font_has_size(int size); diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 5edf8f0..398ad75 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_launcherd.c b/patch_v2/meow_launcherd/meow_launcherd.c index 36482e9..a1470c9 100644 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ b/patch_v2/meow_launcherd/meow_launcherd.c @@ -791,9 +791,14 @@ static void draw_text(int x, int y, const char *s, int scale, uint32_t color) #define MEOW_FONTS_CFG "MeowOS/fonts" #define FONT_PRESETS_MAX 17 /* 1 默认 + ≤16 卡内字体(防爆) */ static const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */ -static const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签 */ +static const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签(含档位大小标记) */ +/* M2.5x 字体档位:每套的 charset 档("common"/"gbk"/"full",取自 font.json,缺省 "common") + * 与 glyph_count(信息用)。供档位分层装载(meow_font_effective_paths_tiered)与设置页标注。 */ +static const char *font_preset_tiers[FONT_PRESETS_MAX]; +static int font_preset_glyphs[FONT_PRESETS_MAX]; static char font_preset_val_buf[FONT_PRESETS_MAX][320]; /* 值持久存储(指向上面指针数组) */ -static char font_preset_label_buf[FONT_PRESETS_MAX][64]; /* 标签持久存储 */ +static char font_preset_label_buf[FONT_PRESETS_MAX][80]; /* 标签持久存储 */ +static char font_preset_tier_buf[FONT_PRESETS_MAX][8]; /* charset 持久存储 */ static int font_presets_n = 0; /* 运行时动态计数(默认=1,仅"默认字体") */ static int mfont_ready = 0; static int mfont_forced = 0; /* font.file 变更→强制真实(un)load,跳过 SDK 复用捷径 */ @@ -821,6 +826,11 @@ static int meow_font_resolve_path(const char *ref, char *out, int olen) return 0; } +/* 前向声明:meow_font_ensure 在下方 font 区中调用(定义在 meow_font_effective_paths_tiered 处)。 */ +static int meow_font_effective_tier(int app_tier); +static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen, + char *out_idx, int ilen, char *out_family, int flen); + /* 解析 font.file 指向的 png + 派生同目录 idx(font.png → font.idx)。 * 仅当指向的 png 存在(access R_OK)返回 0;否则 -1(交由回退链)。 * 返回 0:out_png/out_idx 已填设备绝对路径。 */ @@ -838,22 +848,25 @@ static int meow_font_resolve_selected(char *out_png, int png_len, char *out_idx, return 0; } -/* 尝试加载字库;成功返回 1。优先级(M2.5x): - * 1) font.file(设置键,真正接管加载;相对路径按卡根解析)→ 存在且能 load 即用; +/* 尝试加载字库;成功返回 1。优先级(M2.5x,font.load_full 已接入): + * 0) 生效档 = font.load_full ? gbk : common(launcher 自身 UI 跟随系统大字库开关); + * 1) 生效字体(tier 解析,tier0 即 font.file 所选;相对路径按卡根解析); * 2) 卡 MeowOS/fonts/default/font.png(默认字库); * 3) 固件 /usr/share/meowos/font/font.png。 * A-1/A-2:SDK meow_ui_init() 已用同一 meow_font.o 加载过(可能成功也可能未成功)。 - * 当 font.file 指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload; - * 当 font.file 指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。 - * mfont_forced=1(font.file 变更触发)时无条件真实 reload,跳过上述两条捷径。 + * 当字体指向「默认卡字库」或未设置时复用 SDK 已加载状态,避免重复 unload+reload; + * 当字体指向自定义字库时必须显式 load(SDK 只加载默认,不读 font.file)。 + * mfont_forced=1(font.file/font.load_full 变更触发)时无条件真实 reload,跳过上述捷径。 * 全失败 → mfont_ready=0,ui_draw 回退 5x7 点阵(系统不死)。 */ static int meow_font_ensure(void) { if (mfont_ready && !mfont_forced) return 1; mfont_forced = 0; - char fpng[320], fidx[320]; - int have_sel = (meow_font_resolve_selected(fpng, sizeof fpng, fidx, sizeof fidx) == 0); + char fpng[400], fidx[400], fam[96]; + int tier = meow_font_effective_tier(MEOW_FONT_TIER_COMMON); /* launcher 自身 UI 跟随 load_full */ + int have_sel = (meow_font_effective_paths_tiered(tier, fpng, sizeof fpng, + fidx, sizeof fidx, fam, sizeof fam) == 0); /* SDK 已加载且所选字库为「默认卡字库」(或未设置)→ 复用,避免重复解码。 */ if (meow_gfx_mfont_ready() && (!have_sel || !strcmp(fpng, UI_FONT_PNG_CARD))) { @@ -862,15 +875,15 @@ static int meow_font_ensure(void) return 1; } - /* font.file 优先。 */ + /* 生效字体优先(tier=common 时即 font.file;load_full → 大字库 gbk 档)。 */ if (have_sel && meow_font_load(fpng, fidx) == 0) { mfont_ready = 1; - logmsg("meow_font: loaded font.file %s (%d/%d sizes)", - fpng, meow_font_has_size(28), meow_font_has_size(20)); + logmsg("meow_font: loaded %s (%d/%d sizes), tier=%d", fpng, + meow_font_has_size(28), meow_font_has_size(20), tier); return 1; } if (have_sel) - logmsg("meow_font: font.file %s load failed, fallback", fpng); + logmsg("meow_font: %s load failed, fallback", fpng); /* 卡默认 → 固件。 */ if (meow_font_load(UI_FONT_PNG_CARD, UI_FONT_IDX_CARD) == 0 || meow_font_load(UI_FONT_PNG_SYS, UI_FONT_IDX_SYS) == 0) { @@ -915,17 +928,79 @@ static int meow_font_effective_paths(char *out_png, int plen, char *out_idx, int return -1; } -/* 【tier 选择入口,font 域占位】按字体档位取「当前生效字体」png/idx,供 launch_app 注入 - * MEOW_FONT_PNG/IDX/APP(配合 MEOW_FONT_TIER)。基座只实现 tier=COMMON(=meow_font_effective_paths); - * tier>0 的大字库(GBK/Full)绝对路径由后续 font 代理据此演进填充(约定 MeowOS/fonts// - * /…,或 settings 键 font.tier_gbk / font.tier_full —— 见 MEOW_APP_SDK.md 字体分层)。 - * 未实现前暂回退全局字体。返回 0 成功,-1 无可用。 */ +/* 档位 → font.json charset 字符串名(common/gbk/full)。 */ +static const char *font_tier_charset(int tier) +{ + if (tier >= MEOW_FONT_TIER_FULL) return "full"; + if (tier >= MEOW_FONT_TIER_GBK) return "gbk"; + return "common"; +} + +/* 生效档:app 声明档(0/1/2,越界 clamp)合并「加载大字库」开关(font.load_full)。 + * 开 → 强制 ≥GBK(gbk≈20k 档大字库);关(默认)→ 保持 app 声明(缺省 common≈4k)。 + * 断言:0<=返回<=2。 */ +static int meow_font_effective_tier(int app_tier) +{ + if (app_tier < MEOW_FONT_TIER_COMMON) app_tier = MEOW_FONT_TIER_COMMON; + if (app_tier > MEOW_FONT_TIER_FULL) app_tier = MEOW_FONT_TIER_FULL; + int lf = 0; + if (meow_settings_get_bool("font.load_full", &lf) == 0 && lf) + if (app_tier < MEOW_FONT_TIER_GBK) app_tier = MEOW_FONT_TIER_GBK; + return app_tier; +} + +/* 【tier 选择入口,M2.5x 已实现】按字体档位取「当前生效字体」png/idx,供 launch_app 注入 + * MEOW_FONT_PNG/IDX/APP(配合 MEOW_FONT_TIER)。口径(MEOWOS_SPEC_REF §5.2/§5.7.8): + * - 生效档 = max(app 声明档, font.load_full ? GBK : COMMON):系统「加载大字库」开 → + * 强制 ≥gbk; + * - tier<=common → 走既有 meow_font_effective_paths(font.file/卡默认/固件); + * - tier>0 → 在已扫描的 MeowOS/fonts// 里找 charset 匹配(gbk 请求接受 gbk/full; + * full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回(family = 目录名); + * - 找不到匹配的大字库 → 回退 meow_font_effective_paths(避免 app 直接点到固件小库)。 + * 返回 0 成功(out_png/out_idx/out_family 已填),-1 无可用。 */ static int meow_font_effective_paths_tiered(int tier, char *out_png, int plen, char *out_idx, int ilen, char *out_family, int flen) { - if (tier == MEOW_FONT_TIER_COMMON || tier < MEOW_FONT_TIER_COMMON) + int eff = meow_font_effective_tier(tier); /* 合并 font.load_full:开 → 强制 ≥GBK */ + if (eff <= MEOW_FONT_TIER_COMMON) return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen); - /* TODO(font 代理):按 tier 解析大字库绝对路径(GBK/Full);暂回退全局(tier0)。 */ + /* 大字库:从扫描结果选 charset 匹配的一套。 + * 打分(高优先):gbk 请求 → gbk=2 > full=1;full 请求 → full=2 > gbk=1(降级可接受)。 */ + const char *want = font_tier_charset(eff); + char ref[320]; + int found_family = -1, best_score = 0; + for (int i = 0; i < font_presets_n; i++) { + const char *cs = font_preset_tiers[i] ? font_preset_tiers[i] : "common"; + int score = 0; + if (!strcmp(cs, want)) score = 2; + else if (!strcmp(cs, "full") || !strcmp(cs, "gbk")) score = 1; /* 降级 */ + if (score > best_score) { best_score = score; found_family = i; } + } + if (found_family >= 0) + snprintf(ref, sizeof ref, "%s", font_presets[found_family]); + if (found_family >= 0) { + /* 相对路径按卡根解析为设备绝对路径(与 meow_font_effective_paths 同源)。 */ + if (meow_font_resolve_path(ref, out_png, plen) != 0) return -1; + if (access(out_png, R_OK) != 0) goto fallback; + snprintf(out_idx, ilen, "%s", out_png); + size_t n = strlen(out_idx); + if (n > 4 && !strcmp(out_idx + n - 4, ".png")) { + out_idx[n-4] = '.'; out_idx[n-3] = 'i'; out_idx[n-2] = 'd'; + out_idx[n-1] = 'x'; out_idx[n] = 0; + } + const char *prefix = "/MeowOS/fonts/"; + const char *p = strstr(out_png, prefix); + if (p) { + const char *q = p + strlen(prefix); + const char *end = strrchr(q, '/'); + int rest = end ? (int)(end - q) : 0; + if (rest > 0 && rest < flen) { memcpy(out_family, q, rest); out_family[rest] = 0; } + else snprintf(out_family, flen, "default"); + } else snprintf(out_family, flen, "default"); + return 0; + } +fallback: + /* 无匹配大字库/不可读 → 回退全局(font.file 所选;其可能是大字库或默认)。 */ return meow_font_effective_paths(out_png, plen, out_idx, ilen, out_family, flen); } @@ -942,7 +1017,10 @@ static void meow_font_reload_multifont(void) int on = 0; meow_settings_get_bool("display.multi_font", &on); char gpng[400], gidx[400], gfam[96]; - if (meow_font_effective_paths(gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0) + /* 全局跟生效档(合并 font.load_full → 大字库),与 meow_font_ensure 同源,避免槽0实际装载与 + * 此处理解不一致(多字体去重以全局 png 为基准)。 */ + if (meow_font_effective_paths_tiered(meow_font_effective_tier(MEOW_FONT_TIER_COMMON), + gpng, sizeof gpng, gidx, sizeof gidx, gfam, sizeof gfam) != 0) return; /* 无全局字体:无法多字体 */ /* 各类候选(静态缓冲保活;config_multifont 同步读取,不持指针) */ static char cpng[MEOW_FCLS_COUNT][400], cidx[MEOW_FCLS_COUNT][400]; @@ -1666,9 +1744,12 @@ static int font_sha_eq(const char *a, const char *b) /* 解析 font.json:提取 name / sha256_png / sha256_idx(文件缺失或解析不出则留空)。 */ static void meow_font_json_probe(const char *json_path, char *name, int nlen, - char *sha_png, int slen, char *sha_idx, int xlen) + char *sha_png, int slen, char *sha_idx, int xlen, + char *charset, int clen, int *glyph_count) { name[0] = 0; sha_png[0] = 0; sha_idx[0] = 0; + if (charset) charset[0] = 0; + if (glyph_count) *glyph_count = 0; FILE *f = fopen(json_path, "r"); if (!f) return; char buf[256]; @@ -1676,14 +1757,39 @@ static void meow_font_json_probe(const char *json_path, char *name, int nlen, if (!name[0]) json_str(buf, "name", name, nlen); if (!sha_png[0]) json_str(buf, "sha256_png", sha_png, slen); if (!sha_idx[0]) json_str(buf, "sha256_idx", sha_idx, xlen); + /* M2.5x 字体档位:charset(common|gbk|full)+ glyph_count(信息用,驱动档位分层/标注)。 */ + if (charset && !charset[0]) { + char v[8] = ""; + if (json_str(buf, "charset", v, sizeof v) > 0 && v[0]) + snprintf(charset, clen, "%s", v); + } + if (glyph_count && *glyph_count == 0) { + char v[16] = ""; + if (json_str(buf, "glyph_count", v, sizeof v) > 0 && v[0]) + *glyph_count = atoi(v); + } } fclose(f); } -/* 探测一个 / 套件是否合法:三件套齐全 → 解析 label + 校验 sha。 - * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 */ +/* 档位大小标记(设置页字体选项标注):common≈4k / gbk≈20k / full。 + * 由 font.json 的 charset + glyph_count 派生;缺 charset 回退 common。 */ +static void font_tier_marker(const char *charset, int glyph_count, char *out, int olen) +{ + const char *tier = (charset && charset[0]) ? charset : "common"; + if (!strcmp(tier, "full")) { snprintf(out, olen, "full"); return; } + if (glyph_count > 0) + snprintf(out, olen, "%s≈%dk", tier, (glyph_count + 500) / 1000); + else + snprintf(out, olen, "%s", tier); +} + +/* 探测一个 / 套件是否合法:三件套齐全 → 解析 label + 档位 + 校验 sha。 + * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 + * 输出:label=显示名+大小标记 `名字(common≈4k)`;charset/glyph_count=档位信息。 */ static int meow_font_probe_family(const char *dirpath, const char *family, - char *label, int llen) + char *label, int llen, + char *charset, int clen, int *glyph_count) { char ppath[400], ipath[400], jpath[400]; snprintf(ppath, sizeof ppath, "%s/font.png", dirpath); @@ -1694,9 +1800,15 @@ static int meow_font_probe_family(const char *dirpath, const char *family, return 0; } char name[64] = "", jsha_png[80] = "", jsha_idx[80] = ""; + char cs[8] = ""; int gc = 0; meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png, - jsha_idx, sizeof jsha_idx); - snprintf(label, llen, "%s", name[0] ? name : family); /* 显示名:name 或回退目录名 */ + jsha_idx, sizeof jsha_idx, cs, sizeof cs, &gc); + /* 显示名 = 名 + 档位大小标记(如 `default(common≈4k)` / `gbk≈20k` / `full`) */ + char marker[24]; + font_tier_marker(cs, gc, marker, sizeof marker); + snprintf(label, llen, "%s(%s)", name[0] ? name : family, marker); + if (charset) snprintf(charset, clen, "%s", cs[0] ? cs : "common"); + if (glyph_count) *glyph_count = gc; /* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */ char apng[80] = "", aidx[80] = ""; if (font_sha256_file(ppath, apng, sizeof apng) != 0) @@ -1707,7 +1819,7 @@ static int meow_font_probe_family(const char *dirpath, const char *family, logmsg("fonts: '%s' sha256(idx) 不可用,跳过校验", family); else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx)) logmsg("fonts: '%s' WARN sha256_idx 不匹配(仍可选)", family); - logmsg("fonts: add '%s' (%s)", label, family); + logmsg("fonts: add '%s' (%s) charset=%s glyph_count=%d", label, family, cs[0]?cs:"common", gc); return 1; } @@ -1717,7 +1829,20 @@ static void meow_font_scan_presets_root(const char *card_root) { font_presets_n = 0; font_presets[0] = "MeowOS/fonts/default/font.png"; /* 默认字库 = MeowOS/fonts/default */ - font_preset_labels[0] = "默认字体"; + font_preset_labels[0] = font_preset_label_buf[0]; /* 指向缓冲(运行时派生含大小标记) */ + /* 默认套也读 font.json 档位(存在则跟真实档,缺则 common);档位信息供分层装载/标注。 */ + { + char jp[400], cs[8] = ""; int gc = 0; + char nm0[64] = "", sp0[80] = "", si0[80] = ""; + snprintf(jp, sizeof jp, "%s/%s/default/font.json", card_root, MEOW_FONTS_CFG); + meow_font_json_probe(jp, nm0, sizeof nm0, sp0, sizeof sp0, si0, sizeof si0, + cs, sizeof cs, &gc); + char mk[24]; font_tier_marker(cs, gc, mk, sizeof mk); + snprintf(font_preset_label_buf[0], sizeof font_preset_label_buf[0], "默认字体(%s)", mk); + snprintf(font_preset_tier_buf[0], sizeof font_preset_tier_buf[0], "%s", cs[0]?cs:"common"); + font_preset_tiers[0] = font_preset_tier_buf[0]; + font_preset_glyphs[0] = gc; + } font_presets_n = 1; char root[340]; snprintf(root, sizeof root, "%s/%s", card_root, MEOW_FONTS_CFG); @@ -1726,16 +1851,20 @@ static void meow_font_scan_presets_root(const char *card_root) struct dirent *de; while ((de = readdir(d)) && font_presets_n < FONT_PRESETS_MAX) { if (de->d_name[0] == '.') continue; - char dirpath[400], label[64]; + char dirpath[400], label[80], cs[8] = ""; int gc = 0; snprintf(dirpath, sizeof dirpath, "%s/%s", root, de->d_name); - if (meow_font_probe_family(dirpath, de->d_name, label, sizeof label) == 0) + if (meow_font_probe_family(dirpath, de->d_name, label, sizeof label, + cs, sizeof cs, &gc) == 0) continue; /* 缺组件/损坏 → 跳过该套 */ int i = font_presets_n; snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], "%s/%s/font.png", MEOW_FONTS_CFG, de->d_name); snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label); + snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs[0]?cs:"common"); font_presets[i] = font_preset_val_buf[i]; font_preset_labels[i] = font_preset_label_buf[i]; + font_preset_tiers[i] = font_preset_tier_buf[i]; + font_preset_glyphs[i] = gc; font_presets_n++; } closedir(d); @@ -2321,17 +2450,14 @@ static void settings_sync_cache_listener(const char *key) else if (!strcmp(key, "power.forceoff_ms")) { int v; if (meow_settings_get_int("power.forceoff_ms", &v) == 0) power_forceoff_ms = v; } } -/* ---- 通知 dump 回调占位(通知#6 转储 TF 卡;接线/占位,不做 TF 写)---- +/* ---- 通知 dump 回调(通知#6 转储 TF 卡;做成真)---- * meow_notif 在 TTL 到点(meow_notif_tick,§8 10s)调用此回调归档一条通知,随后释放槽。 - * 基座只把回调挂上来(见 settings_load 的 meow_notif_set_dump_cb);真实 TF 落盘 - * (按 NOTIFICATION_SYSTEM.md §8 / TF_CARD_LAYOUT.md 写 <卡>/MeowOS/notify/...)由后续 - * 通知代理在回调体内实现。注意 meow_notif.c 的 archive_one 自身已 log 一行 "dump: ...", - * 故本回调保持轻量(避免重复刷屏),落盘逻辑填在下方 TODO 处。 */ + * 本回调把 meow_notif 结构体转成一行 CSV 追加写入 <卡>/MeowOS/notifications/notifications.log + * (含 mkdir -p 与按总大小滚动截断、保留最新 10MB),落盘实现在 meow_notif.c 的 + * meow_notif_dump_tf()。无卡/不可写仅打点降级(NOTIFICATION_SYSTEM.md §5/§8 + MEWSPEC §5.3)。 */ static void notif_dump_tf_placeholder(const meow_notif_t *n) { - (void)n; - /* TODO(通知代理, M2c):在此写 TF 卡 /notify//.json 等; - * 基座不实现,仅保留挂接点(触发时机=meow_notif_tick 内 10s 到点/止机转储)。 */ + meow_notif_dump_tf(n); } /* 启动加载(M2.11/M2.24):统一由 meow_settings 后端读 settings.conf; @@ -2342,7 +2468,7 @@ static void settings_load(void) { meow_settings_init(); /* 幂等:注册默认 + 读 settings.conf */ meow_notif_init(); /* M2a:通知存储初始化(幂等,读 notify.* 配额) */ - meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 基座(通知#6):挂 dump 回调(TF 转储占位,M2c 落盘由后续代理实现) */ + meow_notif_set_dump_cb(notif_dump_tf_placeholder); /* 通知#6:挂 dump 回调 = TF 转储(meow_notif_dump_tf:一行 CSV 追加 + 10MB 滚动截断) */ if (!settings_cache_registered) { meow_settings_add_listener(settings_sync_cache_listener); settings_cache_registered = 1; @@ -4483,6 +4609,10 @@ static const srow_t SMENU[] = { /* 字体主题 */ { "字体主题", NULL, SC_HEADER, 0,0,0, NULL,NULL,0 }, { "字体文件", "font.file", SC_PRESET, 0,0,0, font_presets, font_preset_labels, 0 }, + /* M2.5x 字体档位分层/按需大字库:「加载大字库」= font.load_full(int 0/1 默认 0)。 + * 开 → 强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); + * 关(默认)→ 小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。 */ + { "加载大字库", "font.load_full", SC_SWITCH, 0,0,0, NULL,NULL,0 }, /* M2.5x 多字体:开关 + 6 个字符类别字体(preset=可用 MeowOS/fonts// 列表; * 值空 = 跟随全局 font.file)。仅开关开启时 6 行可见(s_row_visible)。 */ { "多字体", "display.multi_font", SC_SWITCH, 0,0,0, NULL,NULL,0 }, @@ -5433,6 +5563,20 @@ static void s_commit(const srow_t *r, int save_now) meow_font_reload_multifont(); /* M2.5x:全局字体变更 → 重配多字体(各类跟随全局者随之更新) */ meow_dirty_full(); /* 字体变化可能影响所有文本渲染,整帧重绘 */ } + /* M2.5x「加载大字库」开关(font.load_full)变更 → 强制重载 launcher 自身字体(小/大字库切换), + * 并重配多字体;大字库 atlas 大,meow_font_ensure 先 unload 旧再 load 新(防双驻留)。 */ + else if (!strcmp(r->key, "font.load_full")) { + mfont_ready = 0; + mfont_forced = 1; /* 跳过 SDK 复用捷径,强制按生效档(gbk/common)真实 reload */ + if (!meow_font_ensure()) + logmsg("font.load_full: reload failed -> dotmatrix fallback"); + else { + int lf = 0; meow_settings_get_bool("font.load_full", &lf); + logmsg("font.load_full: reloaded (load_full=%d)", lf); + } + meow_font_reload_multifont(); + meow_dirty_full(); + } /* M2.5x 多字体:开关或任一类别字体变更 → 重新去重装载类别字库并重绘(多字体可能影响文本渲染)。 */ else if (!strcmp(r->key, "display.multi_font") || !strncmp(r->key, "font.class_", 11)) { meow_font_reload_multifont(); @@ -6846,12 +6990,14 @@ static int launch_app(struct app *a) meow_settings_get_bool("display.multi_font", &mf_on); snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0); envp[ne++] = env_mf; - /* M2.x(基座):字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入; - * 0=默认4K常用(未声明/缺省),1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档, - * 真正按档装载大字库由后续 font 代理实现(见 meow_font_effective_paths_tiered)。 */ - snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", a->fonts); + /* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并 + * 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用, + * 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见 + * meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */ + int eff_tier = meow_font_effective_tier(a->fonts); + snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", eff_tier); envp[ne++] = env_tier; - have_global = (meow_font_effective_paths_tiered(a->fonts, fpng, sizeof fpng, + have_global = (meow_font_effective_paths_tiered(eff_tier, fpng, sizeof fpng, fidx, sizeof fidx, fam, sizeof fam) == 0); if (have_global) { snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng); diff --git a/patch_v2/meow_launcherd/meow_notif.c b/patch_v2/meow_launcherd/meow_notif.c index f9a5c55..907ee95 100644 --- a/patch_v2/meow_launcherd/meow_notif.c +++ b/patch_v2/meow_launcherd/meow_notif.c @@ -24,6 +24,11 @@ #include /* strcasecmp */ #include #include +#include /* mkdir / stat */ +#include +#include /* open/close/write/pread/lseek/ftruncate */ +#include /* O_* */ +#include /* errno/strerror */ #include "meow_notif.h" #include "meow_settings.h" /* B2a:读 notify.* 配额(MEOW_OK/MEOW_ERR_*) */ @@ -89,6 +94,192 @@ static void n_strncpy(char *dst, const char *src, size_t len) dst[len - 1] = 0; } +/* ================= TF 卡转储(通知#6 / M2c,NOTIFICATION_SYSTEM.md §5/§8)================= + * 每条通知一行 CSV 追加到 <卡根>/MeowOS/notifications/notifications.log,并按日志总大小 + * 滚动截断(保留最新 10MB)。系统文件统一放 <卡根>/MeowOS/ 下(TF_CARD_LAYOUT.md)。 */ +#define MEOW_NOTIF_TF_DIR "/usr/data/mnt/sd_0/MeowOS/notifications" +#define MEOW_NOTIF_TF_LOG MEOW_NOTIF_TF_DIR "/notifications.log" +#define MEOW_NOTIF_LOG_MAX_BYTES ((size_t)10 * 1024 * 1024) /* §8:保留最新 10MB */ + +/* 单行 CSV 上限:app_id(32)/category(24)/summary(64)/body(96)/target_activity(48) + * 全引号转义后仍远小于此。 */ +#define MEOW_NOTIF_CSV_LINE_MAX 1024 + +static const char *g_tf_path = MEOW_NOTIF_TF_LOG; +static size_t g_tf_max = MEOW_NOTIF_LOG_MAX_BYTES; + +/* 递归 mkdir -p:沿 path 把每一级目录建出来(EEXIST 无害;无卡/只读则静默失败) */ +static void tf_mkdirs(const char *path) +{ + char tmp[512]; + n_strncpy(tmp, path, sizeof tmp); + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(tmp, 0755); + *p = '/'; + } + } +} + +/* 全量写入(处理短写/EINTR) */ +static int tf_write_all(int fd, const char *buf, size_t len) +{ + size_t off = 0; + while (off < len) { + ssize_t w = write(fd, buf + off, len - off); + if (w < 0) { + if (errno == EINTR) continue; + return -1; + } + off += (size_t)w; + } + return 0; +} + +/* CSV 字段:去换行(\r\n->空格,保证单行)+ 转义(双引号包裹,内部 " 翻倍) */ +static void csv_field(char *dst, size_t dstsz, const char *src) +{ + if (!dst || dstsz == 0) return; + size_t o = 0; + dst[o++] = '"'; + for (const char *p = src ? src : ""; *p; p++) { + if (o + 4 > dstsz) break; /* 留 2 字节给翻倍引号 + 收尾引号 + NUL */ + if (*p == '\r' || *p == '\n') dst[o++] = ' '; /* 去换行 */ + else if (*p == '"') { dst[o++] = '"'; dst[o++] = '"'; } + else dst[o++] = *p; + } + if (o + 1 >= dstsz) o = dstsz - 1; + dst[o++] = '"'; + dst[o] = '\0'; +} + +/* 生成一行 CSV: + * timestamp,id,app_id,category,urgency,title,body,target_activity,ongoing,media,dismissed + * title<-summary;urgency:meow_notif 结构无该字段(保持既有 API),列值留空 ""。 + * 返回写入长度(不含 NUL)。 */ +static size_t tf_line(const meow_notif_t *n, char *buf, size_t bufsz) +{ + char a_app [2 + 2 * MEOW_NOTIF_APP_ID_MAX + 2]; + char a_cat [2 + 2 * MEOW_NOTIF_CAT_MAX + 2]; + char a_sum [2 + 2 * MEOW_NOTIF_SUMMARY_MAX + 2]; + char a_body[2 + 2 * MEOW_NOTIF_BODY_MAX + 2]; + char a_tgt [2 + 2 * MEOW_NOTIF_ACTIVITY_MAX + 2]; + csv_field(a_app, sizeof a_app, n->app_id); + csv_field(a_cat, sizeof a_cat, n->category); + csv_field(a_sum, sizeof a_sum, n->summary); + csv_field(a_body, sizeof a_body, n->body); + csv_field(a_tgt, sizeof a_tgt, n->target_activity); + int r = snprintf(buf, bufsz, + "%llu,%u,%s,%s,\"\",%s,%s,%s,%u,%u,%u\n", + (unsigned long long)n->created_ms, + (unsigned)n->id, + a_app, a_cat, a_sum, a_body, a_tgt, + n->ongoing ? 1u : 0u, + n->media ? 1u : 0u, + n->dismissed ? 1u : 0u); + if (r < 0) return 0; + return ((size_t)r < bufsz) ? (size_t)r : bufsz - 1; +} + +/* 滚动截断:文件超过 g_tf_max 时删除最旧行(从尾部读窗口定位第一条换行作为保留起点), + * 把结尾 ≤max 字节的完整行重写回文件头。窗口内无换行则保留整窗(只保证 ≤max)。 */ +static int tf_trim(const char *path) +{ + int fd = open(path, O_RDWR); + if (fd < 0) return -1; + struct stat st; + if (fstat(fd, &st) != 0) { close(fd); return -1; } + off_t size = st.st_size; + if (size <= (off_t)g_tf_max) { close(fd); return 0; } + + off_t start = size - (off_t)g_tf_max; /* 保留窗口起点 */ + off_t keep = start; /* 最终保留起点(行对齐后) */ + int found = 0; + char chunk[8192]; + for (off_t pos = start; pos < size && !found; ) { + size_t want = (size_t)((size - pos) < (off_t)sizeof chunk ? (size - pos) : (off_t)sizeof chunk); + ssize_t rd = pread(fd, chunk, want, pos); + if (rd <= 0) break; + for (ssize_t i = 0; i < rd; i++) { + if (chunk[i] == '\n') { keep = pos + i + 1; found = 1; break; } + } + pos += rd; + if (rd < (ssize_t)want) break; /* 读到文件尾 */ + } + + /* 重写:把 [keep, size) 拷贝回文件头,再 ftruncate 掉尾部旧数据 */ + off_t remain = size - keep; + if (lseek(fd, 0, SEEK_SET) < 0) { close(fd); return -1; } + off_t wpos = 0; + char buf[8192]; + while (remain > 0) { + size_t want = (size_t)(remain < (off_t)sizeof buf ? remain : (off_t)sizeof buf); + ssize_t rd = pread(fd, buf, want, keep + wpos); + if (rd <= 0) break; + if (tf_write_all(fd, buf, (size_t)rd) < 0) { close(fd); return -1; } + wpos += rd; + remain -= rd; + } + ftruncate(fd, wpos); + close(fd); + return 0; +} + +/* 把一条通知追加为一行 CSV 到 TF 日志(含 mkdir -p 与 10MB 滚动截断)。 + * 返回 NOTIF_OK(0);无卡/不可写/参数非法返回负(降级:仅打点,不崩)。 */ +int meow_notif_dump_tf(const meow_notif_t *n) +{ + if (!n) return NOTIF_ERR_INVAL; + if (!g_tf_path || !g_tf_path[0]) return NOTIF_ERR_INVAL; + tf_mkdirs(g_tf_path); /* 目录不存在则 mkdir -p*/ + + char line[MEOW_NOTIF_CSV_LINE_MAX]; + size_t len = tf_line(n, line, sizeof line); + if (len == 0) return NOTIF_ERR_INVAL; + + int fd = open(g_tf_path, O_WRONLY | O_CREAT | O_APPEND, 0644); + if (fd < 0) { + notif_log("tf: append open fail %s (%s)", g_tf_path, strerror(errno)); + return NOTIF_ERR_IO; + } + if (tf_write_all(fd, line, len) != 0) { + close(fd); + notif_log("tf: append write fail %s (%s)", g_tf_path, strerror(errno)); + return NOTIF_ERR_IO; + } + long size = -1; + struct stat st; + if (fstat(fd, &st) == 0) size = (long)st.st_size; + close(fd); + + if (size > (long)g_tf_max) { /* >10MB:滚动截断,保留最新 */ + tf_trim(g_tf_path); + notif_log("tf: rolled to <= %zuB", g_tf_max); + } + return NOTIF_OK; +} + +/* 开机清板载通知:清空转储日志文件,保留目录(通知#6 / §8:启动时清板载) */ +void meow_notif_clear_tf(void) +{ + if (!g_tf_path || !g_tf_path[0]) return; + tf_mkdirs(g_tf_path); + int fd = open(g_tf_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) close(fd); + else notif_log("tf: clear fail %s (%s)", g_tf_path, strerror(errno)); +} + +/* 覆盖转储日志路径/滚动上限(宿主单测用;NULL/0 恢复默认) */ +void meow_notif_set_tf_path(const char *path) +{ + g_tf_path = (path && path[0]) ? path : MEOW_NOTIF_TF_LOG; +} +void meow_notif_set_tf_max(size_t max_bytes) +{ + g_tf_max = max_bytes ? max_bytes : MEOW_NOTIF_LOG_MAX_BYTES; +} + /* ---- 配额读取(B2a;键缺省/取失败用默认值)---- */ static void read_quota(void) { @@ -113,7 +304,11 @@ int meow_notif_init(void) g_dumped_total = 0; read_quota(); /* B2a:notify.* 配额 */ g_inited = 1; - notif_log("init: slots=%d default dump_cb=log-only (M2c -> TF)", MEOW_NOTIF_MAX_SLOTS); + /* 通知#6:启动清板载通知 + 内存表(内存表上面已 memset;这里清 TF 转储日志, + * 保留目录)。每会话从零开始(通知会话级),不被电源事件清(before_poweroff 只管内存)。 */ + meow_notif_clear_tf(); + notif_log("init: slots=%d tf_log=%s (dump_cb wired by launcher -> meow_notif_dump_tf)", + MEOW_NOTIF_MAX_SLOTS, g_tf_path); return NOTIF_OK; } diff --git a/patch_v2/meow_launcherd/meow_notif.h b/patch_v2/meow_launcherd/meow_notif.h index 35c8183..7791816 100644 --- a/patch_v2/meow_launcherd/meow_notif.h +++ b/patch_v2/meow_launcherd/meow_notif.h @@ -32,7 +32,8 @@ enum { NOTIF_ERR_NOMEM = -6, /* 槽满/内存不足 */ NOTIF_ERR_NOTINIT = -7, /* 未 init */ NOTIF_ERR_QUOTA = -12, /* 配额超限(App 类型数 / 类型条数 / 媒体数) */ - NOTIF_ERR_NOENT = -2 /* 通知 id 不存在 */ + NOTIF_ERR_NOENT = -2, /* 通知 id 不存在 */ + NOTIF_ERR_IO = -5 /* 文件/存储 IO 失败(TF 转储,无卡/不可写) */ }; /* == 容量/长度(单设备上限;定长,控制静态内存)== */ @@ -118,6 +119,17 @@ void meow_notif_before_poweroff(void); /* 覆盖转储回调(NULL=恢复默认日志;M2c 接 TF 落盘)。 */ void meow_notif_set_dump_cb(meow_notif_dump_cb cb); +/* == TF 卡转储(通知#6 / M2c,NOTIFICATION_SYSTEM.md §5/§8 + MEOWOS_SPEC_REF §5.3)== + * 把 dump_cb 接到 meow_notif_dump_tf 即把每条通知追加为一行 CSV 到 + * <卡根>/MeowOS/notifications/notifications.log,并按日志总大小滚动截断 + * (保留最新 10MB,不被电源事件清;启动时 meow_notif_init 先清板载+内存表)。 + * 字段按 meow_notif 结构(title<-summary;urgency 结构无字段故列留空;均做 CSV + * 转义/去换行)。无卡/不可写仅打点降级,绝不崩。 */ +int meow_notif_dump_tf(const meow_notif_t *n); /* 0=成功;负=失败(无卡/IO/参数) */ +void meow_notif_clear_tf(void); /* 开机清板载通知(清空文件,保留目录) */ +void meow_notif_set_tf_path(const char *path); /* 覆盖转储日志路径;NULL=恢复默认卡路径(宿主单测用) */ +void meow_notif_set_tf_max(size_t max_bytes); /* 覆盖滚动截断上限;0=恢复默认 10MB(宿主单测用) */ + /* 调试:打印当前 Active 表/App 表(前缀)。 */ void meow_notif_dump(const char *log_prefix); diff --git a/patch_v2/meow_launcherd/meow_settings.c b/patch_v2/meow_launcherd/meow_settings.c index e941887..67bace2 100644 --- a/patch_v2/meow_launcherd/meow_settings.c +++ b/patch_v2/meow_launcherd/meow_settings.c @@ -104,6 +104,10 @@ static const meow_setting_def g_defs[] = { { "font.class_latin", SET_STR, "", 0,0, NULL, NULL }, { "font.class_latin_sym", SET_STR, "", 0,0, NULL, NULL }, { "font.class_other", SET_STR, "", 0,0, NULL, NULL }, + /* M2.5x 字体档位分层 / 按需大字库:font.load_full(int 0/1 默认 0)=「加载大字库」开关, + * 1=强制全系统用大字库 gbk≈20k 档(覆盖 app 未声明/覆盖默认 font.file); + * 0=默认小字库 common≈4k,仅 app 声明 fonts:"full" 才大字库。见 §5.2/§5.7.8。 */ + { "font.load_full", SET_INT, "0", 0, 1, NULL, NULL }, { "font.size.title", SET_INT, "28", 0, 0, NULL, NULL }, { "font.size.body", SET_INT, "20", 0, 0, NULL, NULL }, { "ui.style", SET_ENUM, "md3", 0, 0, "md3,miui", "Material You,MIUI" }, diff --git a/patch_v2/meow_launcherd/test_notif_tf.c b/patch_v2/meow_launcherd/test_notif_tf.c new file mode 100644 index 0000000..97a9cbb --- /dev/null +++ b/patch_v2/meow_launcherd/test_notif_tf.c @@ -0,0 +1,158 @@ +/* test_notif_tf.c — 宿主单测:通知#6 TF 转储(CSV 转义 / 10MB 滚动截断 / init 清板载+内存表)。 + * + * 直接 #include "meow_notif.c"(同 TU 平移其 static 常量/函数),stub 掉 meow_settings_get_int + * (配额读——返回负=键缺失即用默认,涵盖 CSV/截断/init 逻辑)。宿主 gcc 编译运行即可,不进固件。 + * + * 运行:gcc -D_GNU_SOURCE -o test_notif_tf test_notif_tf.c && ./test_notif_tf + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +/* stub:meow_notif.c 的 read_quota() 读 notify.* 配额。<0 => 键缺失=> 用默认值 */ +int meow_settings_get_int(const char *key, int *out) +{ + (void)key; (void)out; + return -2; /* MEOW_ERR_NOENT */ +} + +#include "meow_notif.c" /* 把全部实现(含 statics)收进本 TU */ + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \ + else fprintf(stderr, "ok: %s\n", msg); \ +} while (0) + +static char *read_all(const char *path, long *out_len) +{ + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + char *s = malloc((size_t)n + 1); + if (!s) { fclose(f); return NULL; } + size_t got = fread(s, 1, (size_t)n, f); + s[got] = 0; + fclose(f); + if (out_len) *out_len = n; + return s; +} + +static void make_notif(meow_notif_t *n, uint32_t id, const char *sum, const char *body) +{ + memset(n, 0, sizeof *n); + n->id = id; + n->created_ms = 1234567890123ULL; + strcpy(n->app_id, "com.app"); + strcpy(n->category, "general"); + strcpy(n->summary, sum); + strcpy(n->body, body); + strcpy(n->target_activity, "act"); + n->ongoing = 1; +} + +/* --- 1) CSV 转义 + 去换行 --- */ +static void test_csv_escape(const char *path) +{ + meow_notif_set_tf_path(path); + meow_notif_set_tf_max(1024 * 1024); /* 放大上限避免此测触发截断 */ + + meow_notif_t n; + /* summary: 逗号 + 双引号 + 换行;body: CRLF + tab */ + make_notif(&n, 42, "Hel\"lo, world\nline2", "Body\r\nwith\ttab"); + n.media = 1; /* 让 media 列=1 */ + CHECK(meow_notif_dump_tf(&n) == 0, "dump_tf returns 0"); + + char *s = read_all(path, NULL); + CHECK(s != NULL, "file exists after dump"); + const char *expect = + "1234567890123,42,\"com.app\",\"general\",\"\",\"Hel\"\"lo, world line2\"," + "\"Body with\ttab\",\"act\",1,1,0\n"; /* ongoing=1 media=1 dismissed=0 */ + CHECK(s && strcmp(s, expect) == 0, "CSV line matches expected (escape+newline-strip)"); + if (s) { fprintf(stderr, " got: %s", s); fprintf(stderr, " exp: %s", expect); } + free(s); +} + +/* --- 2) 10MB 滚动截断(假文件:把上限压到很小模拟)--- */ +static void test_trim(const char *path) +{ + meow_notif_set_tf_path(path); + meow_notif_set_tf_max(256); /* 用 256B 模拟 10MB 上限 */ + + /* 先写 4 条不同标记,让文件超过 256B */ + meow_notif_t n; + make_notif(&n, 1, "lineAAAA", ""); + meow_notif_dump_tf(&n); + make_notif(&n, 2, "lineBBBB", ""); + meow_notif_dump_tf(&n); + make_notif(&n, 3, "lineCCCC", ""); + meow_notif_dump_tf(&n); + make_notif(&n, 4, "lineDDDD", ""); + meow_notif_dump_tf(&n); + + long len = 0; + char *s = read_all(path, &len); + CHECK(s != NULL && len <= 256, "after roll, file size <= limit (10MB analog)"); + CHECK(s && strstr(s, "lineDDDD") != NULL, "newest row retained after roll"); + CHECK(s && strstr(s, "lineAAAA") == NULL, "oldest row rolled away"); + if (s) fprintf(stderr, " rolled content (%ldB):\n%s", len, s); + + /* 追加一条仍 0 成功 */ + make_notif(&n, 5, "lineEEEE", ""); + CHECK(meow_notif_dump_tf(&n) == 0, "append after roll ok"); + free(s); +} + +/* --- 3) init 清板载 + 清内存表,且 post 正常 --- */ +static void test_init_clear(const char *path) +{ + meow_notif_set_tf_path(path); + meow_notif_set_tf_max(1024 * 1024); + + /* 先塞点板载内容 */ + meow_notif_t n; + make_notif(&n, 99, "junk", ""); + meow_notif_dump_tf(&n); + + /* init:应清板载文件 + 清内存表 */ + CHECK(meow_notif_init() == NOTIF_OK, "meow_notif_init ok"); + long len = -1; + char *s = read_all(path, &len); + CHECK(s != NULL && len == 0, "board log cleared at init (dir kept)"); + CHECK(meow_notif_count(NULL) == 0, "memory table cleared at init"); + + /* init 后 post 一条,应活跃 + 可转储 */ + make_notif(&n, 0, "fresh", ""); + uint32_t id = 0; + CHECK(meow_notif_post(&n, &id) == NOTIF_OK, "post after init ok"); + CHECK(id != 0 && meow_notif_count(NULL) == 1, "one active notif after post"); + free(s); +} + +int main(void) +{ + char tmp[] = "/tmp/meow_notif_tf_test_XXXXXX"; + char *dir = mkdtemp(tmp); + if (!dir) { perror("mkdtemp"); return 2; } + + char p1[512], p2[512], p3[512]; + snprintf(p1, sizeof p1, "%s/csv.log", dir); + snprintf(p2, sizeof p2, "%s/trim.log", dir); + snprintf(p3, sizeof p3, "%s/init.log", dir); + + CHECK(p1[0] != 0, "tmpdir ready"); + test_csv_escape(p1); + test_trim(p2); + test_init_clear(p3); + + /* 清理:删除日志文件 + 目录(保底) */ + unlink(p1); unlink(p2); unlink(p3); rmdir(dir); + + fprintf(stderr, "\n%s (failures=%d)\n", g_fail ? "== TEST FAILED ==" : "== TEST PASSED ==", g_fail); + return g_fail ? 1 : 0; +} diff --git a/patch_v2/sdk/meow_ui.h b/patch_v2/sdk/meow_ui.h index bc17e86..298e379 100644 --- a/patch_v2/sdk/meow_ui.h +++ b/patch_v2/sdk/meow_ui.h @@ -328,22 +328,24 @@ const char *meow_notify_urgency_str(meow_notify_urgency u); const char *meow_notify_category_str(meow_notify_category cat); /* ============================================================ */ -/* §10 字体分层 / 大字库(草案/待定案 —— 本批仅加声明/枚举/宏,不实现) */ +/* §10 字体分层 / 大字库(M2.5x:基座声明 + font 域已实现按档装载) */ /* ============================================================ */ /* 字体「档位」概念:launcher 按 app.json 声明的字库档("fonts":<档>)向 app 注入 - * MEOW_FONT_TIER=<0|1|2>;SDK(meow_ui_gfx / 后续 meow_font 大字库加载)据档选库: + * MEOW_FONT_TIER=<0|1|2>;SDK(meow_ui_gfx / meow_font 大字库加载)据档选库: * MEOW_FONT_TIER_COMMON=0 默认 4K 常用字库; * MEOW_FONT_TIER_GBK =1 大字库 GBK; * MEOW_FONT_TIER_FULL =2 Full(全字符集)。 - * 后续 font 域代理据此在 meow_font.c / gen_font.py 实现大字库生成与按档装载。 - * 既有 meow_font_* 符号(class_of/has_glyph/load_slot/config_multifont…)保留不动; - * 大字库装载以 set_tier/tier 作为入口,或由后续代理按 meow_font_config_multifont 扩展。 */ + * 大字库生成由 font/gen/gen_font.py --charset 按档产出;按档装载由 meow_font_reload_tier() + * 实现(先 unload 旧再 load 新,释放旧 atlas 防大字库双驻留)。launcher 经 + * meow_font_effective_paths_tiered() 按档解析大字库绝对路径并注入 MEOW_FONT_PNG/IDX。 + * 既有 meow_font_* 符号(class_of/has_glyph/load_slot/config_multifont…)保留不动。 + * 判缺字:meow_font_has_glyph(cp) 预判当前字库是否含该码位。 */ #define MEOW_FONT_TIER_COMMON 0 #define MEOW_FONT_TIER_GBK 1 #define MEOW_FONT_TIER_FULL 2 /* 设当前字体档(0/1/2,越界 clamp 到合法值),返回生效档。 - * 基座在 meow_ui_gfx.c 定义(只存档 + 触发重载请求);真正按档装载大字库由后续 font 代理实现。 */ + * meow_ui_gfx.c 定义并触发 meow_font_reload_tier() 真重载(大字库切换释放旧 atlas)。 */ int meow_font_set_tier(int tier); /* 当前字体档(初始 = MEOW_FONT_TIER_COMMON;meow_ui_init 时读 MEOW_FONT_TIER env 对齐)。 */ int meow_font_tier(void); diff --git a/patch_v2/sdk/meow_ui_gfx.c b/patch_v2/sdk/meow_ui_gfx.c index c429599..d6a9798 100644 --- a/patch_v2/sdk/meow_ui_gfx.c +++ b/patch_v2/sdk/meow_ui_gfx.c @@ -77,8 +77,8 @@ static unsigned long long g_sbar_collapse_start_ms = 0; /* 本次过渡起始 /* ---- 字体档位(MEOW_FONT_TIER,基座存档 + 公共 get/set)---- * launcher 按 app.json "fonts":<档> 注入 MEOW_FONT_TIER=<0|1|2>;SDK 读入并以 - * meow_font_set_tier/tier 暴露。真正按档装载大字库(GBK/Full)由后续 font 代理在 - * meow_font.c / meow_gfx_load_multifont 挂钩实现——此处仅记录档位不换库。 */ + * meow_font_set_tier/tier 暴露。按档装载大字库(GBK/Full)由 meow_font_reload_tier() + * 在 meow_font.c 实现;meow_font_set_tier() 在档位变化时经 env/settings 路由到该钩子真重载。 */ static int g_font_tier = MEOW_FONT_TIER_COMMON; /* ---- 帧率调度(A-1/A-2:从 launcher M2.16 迁入,刷新率调度归口 SDK)---- @@ -665,20 +665,32 @@ unsigned long long meow_ui_frames_presented(void) { return g_frames_presented; } int meow_gfx_is_device(void) { return g_fb_mem != NULL; } int meow_gfx_mfont_ready(void) { return g_mfont_ready; } -/* ---- 字体档位(公共 API,基座定义)---- - * 注意:meow_font_tier()/meow_font_set_tier() 已在 meow_ui_gfx.c 定义(符号唯一), - * 后续 font 代理勿在 meow_font.c 重复定义;可在 meow_font.c 实现「按档装载大字库」并在 - * meow_font_set_tier 的钩子处唤起(见下)。 */ +/* ---- 字体档位(公共 API,基座定义 + font 域真重载钩子)---- + * meow_font_tier()/meow_font_set_tier() 在 meow_ui_gfx.c 定义(符号唯一),font 代理勿在 + * meow_font.c 重复定义;大字库按档装载由 meow_font.c meow_font_reload_tier() 实现,并在 + * meow_font_set_tier() 处唤起(见下)。 */ int meow_font_tier(void) { return g_font_tier; } int meow_font_set_tier(int tier) { + int old = g_font_tier; if (tier < MEOW_FONT_TIER_COMMON) tier = MEOW_FONT_TIER_COMMON; if (tier > MEOW_FONT_TIER_FULL) tier = MEOW_FONT_TIER_FULL; if (tier == g_font_tier) return g_font_tier; g_font_tier = tier; - /* TBD(基座):真正按档装载大字库(meow_font_load_tier 或 config_multifont 扩展)由 - * 后续 font 代理在 meow_font.c 实现;此处仅记录档位 + 标记重载(下一轮 load 读 tier)。 */ + /* 真重载(font 域钩子,M2.5x):按档装载大字库。launcher 已按档解析并注入 + * MEOW_FONT_PNG/IDX(tier>0 → GBK/Full 大字库绝对路径);重读 env 重载。 + * 无 env/失败 → 回退卡默认 MeowOS/fonts/default → 固件。 + * 大字库 atlas 大(GBK≈5-15MB+),meow_font_reload_tier 先 unload 旧再 load 新,防双驻留。 */ + const char *ep = getenv("MEOW_FONT_PNG"); + const char *ei = getenv("MEOW_FONT_IDX"); + if (ep && ep[0] && ei && ei[0] && meow_font_reload_tier(tier, ep, ei) == 0) + return g_font_tier; + if (meow_font_reload_tier(tier, MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) + return g_font_tier; + if (meow_font_reload_tier(tier, MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) + return g_font_tier; + g_font_tier = old; /* 全部失败 → 回退旧档(字库可能已不可用,5x7 点阵兜底,系统不死) */ return g_font_tier; } @@ -692,9 +704,9 @@ int meow_font_set_tier(int tier) static int load_font(void) { /* 字体档位(MEOW_FONT_TIER env,由 launcher 按 app.json "fonts" 注入): - * 0=默认4K常用 1=大字库GBK 2=Full。基座只读入存档 + 对齐公共 get/set; - * 真正按档装载大字库(GBK/Full 的字库路径/面)由后续 font 代理在 meow_font.c / - * meow_gfx_load_multifont 挂钩实现——此处仍先装载默认(tier0)字库,档位仅记录。 */ + * 0=默认4K常用 1=大字库GBK 2=Full。launcher 已按档解析大字库路径并经 MEOW_FONT_PNG/IDX + * 注入(见 meow_font_effective_paths_tiered),故下方 meow_font_load(ep,ei) 即装载 + * 对应档位字库(tier>0 → GBK/Full 大字库);此处 read env 存档对齐公共 get/set。 */ const char *etr = getenv("MEOW_FONT_TIER"); if (etr && etr[0]) { g_font_tier = atoi(etr); diff --git a/patch_v2/sdk/meow_ui_input.c b/patch_v2/sdk/meow_ui_input.c index fed0fc6..7a155d2 100644 --- a/patch_v2/sdk/meow_ui_input.c +++ b/patch_v2/sdk/meow_ui_input.c @@ -38,6 +38,20 @@ #define QUEUE_N 128 #define MAX_DEVS 8 +/* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)── + * 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理 + * (路由 meow_ui_sys_notif_pull),不下发给 APP 的 on_swipe(顶部热区为系统保留手势)。 + * 与左缘返回(SWIPE_LEFT)/ meow_ui_swipe_hint 互不影响——后者仍全屏生效,详见下方触摸机。 */ +#define MEOW_EV_NOTIF_PULL 5 + +/* 顶部下滑 → 通知中心判定:纵向 DOWN 手势的起点(origin_y)是否落在顶部热区内。 + * 热区由 meow_ui_statusbar_hotzone() 给出(非全屏=状态栏高、全屏=MEOW_SBAR_HOTZONE_FS、 + * 收合=提示线高)。供宿主单测直调(判据与触摸状态机共用)。 */ +int meow_ui_input_is_notif_pull(int origin_y, int hotzone) +{ + return origin_y >= 0 && origin_y < hotzone; +} + static int g_thread_started = 0; static pthread_t g_thread; static volatile int g_thread_run = 1; @@ -165,7 +179,14 @@ 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); + /* 顶部下滑 → 通知中心(系统手势,不下发 APP):起点在顶部热区内才认。 + * 热区高度随状态变化(非全屏=整条状态栏、全屏=MEOW_SBAR_HOTZONE_FS 缩小、 + * 收合=提示线高)——全屏 app 只在顶部极窄热区被系统占用,故「不影响全屏 app」。 */ + if (dir == MEOW_SWIPE_DOWN && + meow_ui_input_is_notif_pull(t_dy, meow_ui_statusbar_hotzone())) + push_ev(MEOW_EV_NOTIF_PULL, t_dy, ady > adx ? ady : adx); + else + push_ev(MEOW_EV_SWIPE, dir, ady > adx ? ady : adx); } } else { push_ev(MEOW_EV_TAP, t_cx, t_cy); @@ -287,6 +308,11 @@ 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_NOTIF_PULL) { + if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */ + /* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发 + * APP on_swipe;路由到系统钩子(当前为记录占位,通知中心 UI 为后续里程碑)。 */ + meow_ui_sys_notif_pull(); } else if (e.type == MEOW_EV_SWIPE_PROGRESS) { if (in_startup) continue; /* 启动窗口进度条静音 */ int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */ diff --git a/patch_v2/sdk/meow_ui_sys.c b/patch_v2/sdk/meow_ui_sys.c index c271f5e..e21900e 100644 --- a/patch_v2/sdk/meow_ui_sys.c +++ b/patch_v2/sdk/meow_ui_sys.c @@ -156,18 +156,31 @@ void meow_ui_set_fullscreen(int on) int meow_ui_statusbar_height(void) { - return meow_ui_sys_statusbar_active() ? MUI_SBAR_H : 0; + if (!meow_ui_sys_statusbar_active()) return 0; + return meow_ui_statusbar_collapsed() ? MUI_SBAR_COLLAPSE_H : MUI_SBAR_H; } /* ---- 内部(gfx/input 用) ---- */ int meow_ui_sys_statusbar_active(void) { - return g_statusbar_on && !g_fullscreen; + /* collapsed 视为状态栏仍激活(只留提示线):与 fullscreen 并存(全屏启 collapsed)—— + * 这样 gfx.c present() 的收合动画块会运行(驱动 g_sbar_collapse_h 插值),且顶部提示线 + * 正常绘制/热区可用。fullscreen 且不 collapsed → 整条隐藏(沉浸,inset=0),保持既有语义。 */ + return g_statusbar_on && (meow_ui_statusbar_collapsed() || !g_fullscreen); } int meow_ui_sys_statusbar_height(void) { - return meow_ui_sys_statusbar_active() ? MUI_SBAR_H : 0; + if (!meow_ui_sys_statusbar_active()) return 0; + return meow_ui_statusbar_collapsed() ? MUI_SBAR_COLLAPSE_H : MUI_SBAR_H; +} + +void meow_ui_sys_notif_pull(void) +{ + /* 系统级「下拉展开通知中心」手势接线(M2.5x):输入层识别出「起始于顶部热区的 + * SWIPE_DOWN」后路由到此。通知中心 UI 为后续里程碑——此处先落记录(避免哑无痕); + * 届时由对应代理在该入口挂真实展开逻辑(IPC SHOW_ACTIVITY / 覆盖层渲染)。 */ + meow_log(MEOW_LOG_WARN, "notif_pull: notification center TBD (wired)"); } int meow_ui_sys_statusbar_force(void) diff --git a/patch_v2/sdk/meow_ui_sys.h b/patch_v2/sdk/meow_ui_sys.h index 8caeb68..de80a7d 100644 --- a/patch_v2/sdk/meow_ui_sys.h +++ b/patch_v2/sdk/meow_ui_sys.h @@ -34,6 +34,13 @@ typedef struct meow_sys_state { #define MUI_SBAR_H 28 #define MUI_SBAR_FONT 20 +/* 收合(collapsed)状态下状态栏的有效加载高度(inset)= 顶部提示线高度(px)。 + * 收合=状态栏内容上移出屏、只留一条提示线:app 内容据此上移(非满 28), + * 形成「不完全沉浸(留提示线)」;与 fullscreen(整条隐藏、inset0)区别。 + * 取值沿用公共宏 MEOW_SBAR_HOTZONE_FS(=4,全屏时顶部热区/提示线的缩后高度), + * 具体绘制高度(gfx.c 的 MUI_SBAR_HINT_H=6)在渲染端,这里按提示线高度口径即可。 */ +#define MUI_SBAR_COLLAPSE_H MEOW_SBAR_HOTZONE_FS + /* 首次全量读取;失败仍返回 0(字段置 -1/0)。 */ int meow_ui_sys_init(void); @@ -43,12 +50,21 @@ void meow_ui_sys_refresh(void); /* 当前快照(内部只读)。 */ const meow_sys_state *meow_ui_sys_get(void); -/* 状态栏是否应绘制(= set_statusbar(1) 且 非 fullscreen)。 */ +/* 状态栏是否应绘制(= set_statusbar(1) 且 非 fullscreen),收合(collapsed)兼容: + * collapsed 视为状态栏仍在(只留提示线),故 collapsed 时即使 fullscreen 也返回 1, + * 让渲染端/输入层继续驱动上收动画与提示线;fullscreen 且不 collapsed → 0(整条隐藏, + * 沉浸,保持既有语义)。 */ int meow_ui_sys_statusbar_active(void); -/* 状态栏高度(px):active 时 28,否则 0(供 meow_ui_statusbar_height 用)。 */ +/* 状态栏有效高度(px,供 inset/提示条定位):active 时 28;collapsed 时=提示线高度 + * MUI_SBAR_COLLAPSE_H(非满 28,app 内容可上移);否则 0(供 meow_ui_statusbar_height 用)。 */ int meow_ui_sys_statusbar_height(void); +/* 系统级「下拉展开通知中心」手势挂钩(M2.5x 接线):输入层识别出「起始于顶部热区的 + * SWIPE_DOWN」后路由到这里。通知中心 UI 为后续里程碑,此入口作记录/占位;届时由对应 + * 代理在此挂真实展开逻辑(IPC SHOW_ACTIVITY / 覆盖层渲染)。 */ +void meow_ui_sys_notif_pull(void); + /* 自上次渲染后状态栏数据是否已变(1s 轻刷/显隐切换置位);读取即清除。 * 渲染后端 present() 用它决定是否重画顶部条(配合脏区/整帧判断)。 */ int meow_ui_sys_statusbar_force(void);