Add 'sdk/' from commit '2e7561d74f75de05dd7fb362f203f600afb451d7'
git-subtree-dir: sdk git-subtree-mainline:17a972d46fgit-subtree-split:2e7561d74f
This commit is contained in:
commit
f44e3bc6d0
85 files changed
+23811
No files matched your search
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
# build_sdk.sh — 构建 meow_ui.a(mipsel 静态库,含 meow_font.o 内联打包,
|
||||
# 应用只需链 meow_ui.a + libz.a + -lpthread)。
|
||||
set -e
|
||||
ROOT="$HOME/Project/R3ProII"
|
||||
cd "$(dirname "$0")"
|
||||
CC="$ROOT/.tools/zig-mipsel-cc"
|
||||
AR="${AR:-/usr/bin/ar}" # 系统 GNU ar 即可归档 mipsel ELF 对象(架构无关)
|
||||
|
||||
rm -f meow_ui.a *.o
|
||||
|
||||
# -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移)
|
||||
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"
|
||||
CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd"
|
||||
for f in $OBJS; do
|
||||
echo "== cc $f =="
|
||||
"$CC" $CFLAGS -c "$f.c" -o "$f.o"
|
||||
done
|
||||
|
||||
# meow_font.o(复用 launcher 字库,打进 meow_ui.a → 应用只链一个库)
|
||||
TMP="$(mktemp -d)"
|
||||
( cd "$TMP" && "$AR" x "$ROOT/patch_v2/meow_launcherd/meow_font.a" && ls *.o )
|
||||
cp "$TMP"/*.o meow_font.o 2>/dev/null || { echo "meow_font extract failed"; rm -rf "$TMP"; exit 1; }
|
||||
rm -rf "$TMP"
|
||||
|
||||
rm -f meow_ui.a
|
||||
for f in $OBJS; do "$AR" rcs meow_ui.a "$f.o"; done
|
||||
"$AR" rcs meow_ui.a meow_font.o
|
||||
echo "== meow_ui.a =="
|
||||
ls -la meow_ui.a
|
||||
"$AR" t meow_ui.a
|
||||
@@ -0,0 +1,832 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
gen_pinyin_lex.py — MeowOS 中文拼音输入法 · 词库生成器(主机侧,离线)。
|
||||
|
||||
产出:meow_pinyin_lex.h(静态 .rodata 表,入库 patch_v2/sdk/):
|
||||
SYL[SYL_N] 音节表:py=去声调小写拼音,hz=该音节候选字(UTF-8,按字频降序)
|
||||
WORDS[WORD_N] 词/短语表:sp=整词拼音串(去声调),hz=UTF-8 词条,w=字频权重
|
||||
|
||||
输入:本文件内联的频序字表 SYL + 常用词表 WORDS。无真实语料(宿主机无 pypinyin/jieba
|
||||
字表),按设计稿 §2.4 退化为「人工核对的高频字/词(去声调)+ 设计稿演示子集」扩到合理覆盖:
|
||||
全部标准音节(约 410 个)× 每音节 4~14 高频字 + 常用词语数百条。候选字全部落在
|
||||
GB2312 常用字集(font/gen/gen_font.py 的 common/gbk tier 已含),保证上屏可渲染。
|
||||
|
||||
运行:python3 gen_pinyin_lex.py (在工作目录产出 meow_pinyin_lex.h,并打印体积账)
|
||||
约束:字节序无关;词表按 sp 排序(前缀提示走「精确二分 + 线性回溯」)。
|
||||
"""
|
||||
|
||||
# 音节表:py=去声调小写拼音,hz=该音节候选字(按字频降序,UTF-8)。
|
||||
# 覆盖 GB2312 一级+高频二级字的常见音节;生僻音节给 2~5 个高频字即可。
|
||||
SYL = {
|
||||
# ---- a ----
|
||||
'a': '啊阿吖呵腌',
|
||||
'ai': '爱哀挨唉矮碍癌艾呆埃',
|
||||
'an': '安按暗案岸俺鞍庵',
|
||||
'ang': '昂盎肮',
|
||||
'ao': '奥懊傲熬凹澳敖袄',
|
||||
# ---- ba ----
|
||||
'ba': '把八吧爸拔罢霸巴扒芭',
|
||||
'bai': '白百摆败拜柏伯佰',
|
||||
'ban': '办半班搬版板伴扮拌瓣',
|
||||
'bang':'帮棒榜绑膀磅邦蚌',
|
||||
'bao': '报保包宝抱暴薄爆饱堡',
|
||||
'bei': '被北背倍杯备悲辈贝碑',
|
||||
'ben': '本奔笨苯',
|
||||
'beng':'蹦崩甭泵迸',
|
||||
'bi': '比必笔闭毕鼻避彼壁弊',
|
||||
'bian':'变边便遍辨编辩扁鞭',
|
||||
'biao':'表标彪镖膘',
|
||||
'bie': '别憋鳖瘪',
|
||||
'bin': '宾滨彬濒斌',
|
||||
'bing':'病并兵冰饼丙柄秉',
|
||||
'bo': '波播拨薄博伯驳脖膊',
|
||||
'bu': '不步部布补捕怖埠簿',
|
||||
# ---- ca ----
|
||||
'ca': '擦嚓',
|
||||
'cai': '才菜采财猜彩裁踩材',
|
||||
'can': '参残餐灿惨蚕惭',
|
||||
'cang':'藏仓苍舱',
|
||||
'cao': '草操曹槽糙',
|
||||
'ce': '侧测策册厕',
|
||||
'cen': '岑',
|
||||
'ceng':'层曾蹭',
|
||||
'cha': '查差茶插叉察刹诧',
|
||||
'chai':'拆柴差钗',
|
||||
'chan':'产缠颤馋禅蝉',
|
||||
'chang':'长常场厂唱昌畅肠尝偿',
|
||||
'chao':'超朝潮吵炒抄钞巢',
|
||||
'che': '车扯撤彻澈',
|
||||
'chen':'陈沉晨臣尘趁衬辰',
|
||||
'cheng':'成城程称承诚乘盛橙惩',
|
||||
'chi': '吃持池迟赤尺斥驰齿翅',
|
||||
'chong':'冲重虫充宠崇憧',
|
||||
'chou':'抽愁丑臭筹仇酬绸',
|
||||
'chu': '出处初除楚厨触储础畜',
|
||||
'chua': '欻',
|
||||
'chuai':'揣踹',
|
||||
'chuan':'传船穿川串喘',
|
||||
'chuang':'创窗床闯疮',
|
||||
'chui':'吹垂锤炊',
|
||||
'chun':'春纯唇蠢淳',
|
||||
'chuo':'戳绰辍',
|
||||
'ci': '次此词刺辞瓷慈磁雌',
|
||||
'cong':'从聪葱囱匆',
|
||||
'cou': '凑',
|
||||
'cu': '粗促醋簇卒',
|
||||
'cuan':'窜窜蹿',
|
||||
'cui': '催脆翠摧崔粹',
|
||||
'cun': '存村寸蹲',
|
||||
'cuo': '错措搓挫磋',
|
||||
# ---- da ----
|
||||
'da': '大打达答搭沓嗒',
|
||||
'dai': '带代待戴袋呆逮贷岱',
|
||||
'dan': '但担单弹蛋淡胆旦耽诞',
|
||||
'dang':'当挡党荡档挡',
|
||||
'dao': '到道倒刀岛导盗稻悼',
|
||||
'de': '的得地德',
|
||||
'deng':'等灯登邓凳瞪',
|
||||
'di': '地第低底敌抵滴弟帝递',
|
||||
'dian':'点电店典殿掂惦奠',
|
||||
'diao':'调掉钓雕叼吊',
|
||||
'die': '跌爹叠碟蝶迭',
|
||||
'ding':'定顶丁订钉盯鼎',
|
||||
'diu': '丢',
|
||||
'dong':'动东懂冬洞冻董栋',
|
||||
'dou': '都斗豆逗兜陡抖',
|
||||
'du': '度读独毒肚堵赌渡杜',
|
||||
'duan':'段断短端锻',
|
||||
'dui': '对队堆兑',
|
||||
'dun': '顿吨蹲盾钝敦',
|
||||
'duo': '多朵夺躲堕舵跺',
|
||||
# ---- e ----
|
||||
'e': '饿恶额鹅俄蛾遏鄂',
|
||||
'en': '恩摁',
|
||||
'er': '而二儿耳尔饵洱',
|
||||
# ---- fa ----
|
||||
'fa': '发法罚伐乏阀筏',
|
||||
'fan': '反饭犯翻凡烦返泛范繁',
|
||||
'fang':'方放房防访仿坊纺',
|
||||
'fei': '非飞费肥废匪肺菲',
|
||||
'fen': '分粉份奋愤纷芬坟焚',
|
||||
'feng':'风封峰丰疯逢缝蜂凤',
|
||||
'fo': '佛',
|
||||
'fou': '否',
|
||||
'fu': '副复福扶服府负富付妇父符附腐',
|
||||
# ---- ga ----
|
||||
'ga': '嘎尬噶',
|
||||
'gai': '该改盖概钙溉',
|
||||
'gan': '干感敢赶甘杆肝竿',
|
||||
'gang':'刚钢港岗缸杠',
|
||||
'gao': '高告搞稿糕膏',
|
||||
'ge': '个各歌格哥隔割阁胳',
|
||||
'gei': '给',
|
||||
'gen': '跟根跟',
|
||||
'geng':'更耕颈梗耿',
|
||||
'gong':'公共工功攻供宫拱贡恭',
|
||||
'gou': '够狗购勾沟构钩',
|
||||
'gu': '故古顾股骨鼓谷姑孤估',
|
||||
'gua': '挂瓜刮寡卦',
|
||||
'guai':'怪拐乖',
|
||||
'guan':'关管观官冠馆惯罐',
|
||||
'guang':'光广逛',
|
||||
'gui': '贵归鬼规柜桂跪诡',
|
||||
'gun': '滚棍',
|
||||
'guo': '国过果锅裹郭',
|
||||
# ---- ha ----
|
||||
'ha': '哈蛤',
|
||||
'hai': '还海害孩嗨骇',
|
||||
'han': '含喊汉汗寒韩罕憾',
|
||||
'hang':'行航夯杭',
|
||||
'hao': '好号浩豪毫郝耗昊',
|
||||
'he': '和合何河喝盒禾鹤贺荷',
|
||||
'hei': '黑嘿',
|
||||
'hen': '很恨狠痕',
|
||||
'heng':'横恒衡哼',
|
||||
'hong':'红洪宏虹烘鸿',
|
||||
'hou': '后候厚猴喉吼',
|
||||
'hu': '湖户呼胡虎糊互忽壶糊',
|
||||
'hua': '话花化画华滑划桦',
|
||||
'huai':'坏怀淮',
|
||||
'huan':'还换环欢缓幻患唤',
|
||||
'huang':'黄慌皇荒晃煌幌',
|
||||
'hui': '会回挥灰汇毁惠慧辉恢悔',
|
||||
'hun': '混昏婚魂浑',
|
||||
'huo': '或火活货获伙惑霍',
|
||||
# ---- ji ----
|
||||
'ji': '及机级极即急集计记既济季几给基激肌迹际剂',
|
||||
'jia': '家加价假架甲佳嘉嫁夹',
|
||||
'jian':'见件间建减检简坚健剑渐尖肩荐监',
|
||||
'jiang':'将讲江奖降酱姜僵',
|
||||
'jiao':'叫教交角觉较脚焦骄娇郊浇',
|
||||
'jie': '界接解节姐借街杰皆截戒届阶',
|
||||
'jin': '进近金今紧仅尽禁劲津筋',
|
||||
'jing':'经精京静境警惊景镜竟竞睛晶',
|
||||
'jiong':'窘炯',
|
||||
'jiu': '就九久酒旧救究舅揪',
|
||||
'ju': '据居具局举句巨距聚拒剧',
|
||||
'juan':'卷捐娟倦眷',
|
||||
'jue': '觉决绝角掘爵倔嚼',
|
||||
'jun': '军均君俊菌骏',
|
||||
# ---- ka ----
|
||||
'ka': '卡咖喀',
|
||||
'kai': '开凯慨楷',
|
||||
'kan': '看砍刊堪坎勘',
|
||||
'kang':'抗康炕扛慷',
|
||||
'kao': '考靠烤拷',
|
||||
'ke': '可课科克客刻渴棵颗咳',
|
||||
'kei': '剋',
|
||||
'ken': '肯垦啃恳',
|
||||
'keng':'坑吭',
|
||||
'kong':'空控孔恐',
|
||||
'kou': '口扣抠寇',
|
||||
'ku': '苦哭库裤酷枯窟',
|
||||
'kua': '跨夸垮挎',
|
||||
'kuai':'快块筷会脍',
|
||||
'kuan':'宽款',
|
||||
'kuang':'况狂矿框筐旷',
|
||||
'kui': '亏溃愧窥葵',
|
||||
'kun': '困昆捆坤',
|
||||
'kuo': '扩阔括廓',
|
||||
# ---- la ----
|
||||
'la': '拉啦辣蜡腊喇',
|
||||
'lai': '来莱赖',
|
||||
'lan': '蓝栏拦懒烂览兰篮',
|
||||
'lang':'浪狼朗郎',
|
||||
'lao': '老劳牢捞唠姥',
|
||||
'le': '了乐勒',
|
||||
'lei': '类累泪雷蕾垒',
|
||||
'leng':'冷愣棱',
|
||||
'li': '里力理立利李离例历丽励粒厉礼',
|
||||
'lia': '俩',
|
||||
'lian':'连脸练联恋链帘莲',
|
||||
'liang':'量两亮辆凉粮良梁',
|
||||
'liao':'了料聊疗辽廖',
|
||||
'lie': '列烈裂猎劣',
|
||||
'lin': '林临邻淋琳磷',
|
||||
'ling':'令领另零灵铃岭凌',
|
||||
'liu': '六流留刘柳溜瘤',
|
||||
'long':'龙隆笼聋拢垄',
|
||||
'lou': '楼漏搂陋',
|
||||
'lu': '路陆录露鲁卢炉鹿炉',
|
||||
'luan':'乱卵峦',
|
||||
'lun': '论轮伦沦',
|
||||
'lue': '略掠',
|
||||
'luo': '落罗洛络骆裸',
|
||||
'lv': '率律绿虑旅驴吕铝',
|
||||
'lve': '略掠',
|
||||
# ---- ma ----
|
||||
'ma': '吗妈马麻骂码嘛玛',
|
||||
'mai': '买卖麦埋迈脉',
|
||||
'man': '满慢蛮瞒漫曼',
|
||||
'mang':'忙茫盲芒莽',
|
||||
'mao': '毛猫冒帽茂矛贸锚',
|
||||
'me': '么',
|
||||
'mei': '没每美妹梅眉媒霉',
|
||||
'men': '们门闷萌',
|
||||
'meng':'梦猛蒙盟萌',
|
||||
'mi': '米密迷秘蜜谜觅',
|
||||
'mian':'面免棉眠缅勉',
|
||||
'miao':'秒妙苗描庙瞄',
|
||||
'mie': '灭蔑',
|
||||
'min': '民敏闽皿',
|
||||
'ming':'明名命鸣铭茗',
|
||||
'miu': '谬',
|
||||
'mo': '摸模磨末莫默墨陌摩',
|
||||
'mou': '某谋牟',
|
||||
'mu': '目母木幕牧慕墓穆亩',
|
||||
# ---- na ----
|
||||
'na': '那拿哪纳娜呐',
|
||||
'nai': '奶乃耐奈',
|
||||
'nan': '难南男楠',
|
||||
'nang':'囊',
|
||||
'nao': '脑闹恼挠',
|
||||
'ne': '呢',
|
||||
'nei': '内馁',
|
||||
'nen': '嫩',
|
||||
'neng':'能',
|
||||
'ni': '你呢泥尼妮霓逆拟腻匿',
|
||||
'nian':'年念粘捻撵',
|
||||
'niang':'娘酿',
|
||||
'niao':'鸟尿',
|
||||
'nie': '捏聂孽',
|
||||
'nin': '您',
|
||||
'ning':'宁凝拧',
|
||||
'niu': '牛扭纽妞',
|
||||
'nong':'弄农浓脓',
|
||||
'nou': '耨',
|
||||
'nu': '怒努奴',
|
||||
'nuan':'暖',
|
||||
'nuo': '诺挪懦',
|
||||
'nv': '女',
|
||||
'nve': '虐疟',
|
||||
# ---- o ----
|
||||
'o': '哦噢',
|
||||
'ou': '欧偶呕殴藕',
|
||||
# ---- pa ----
|
||||
'pa': '怕爬趴帕啪',
|
||||
'pai': '排派拍牌徘湃',
|
||||
'pan': '判盘盼攀畔叛',
|
||||
'pang':'旁胖庞乓',
|
||||
'pao': '跑炮泡抛袍',
|
||||
'pei': '陪配佩赔培',
|
||||
'pen': '喷盆',
|
||||
'peng':'朋碰捧棚蓬鹏',
|
||||
'pi': '皮批脾披疲劈匹屁',
|
||||
'pian':'片篇偏骗',
|
||||
'piao':'票飘漂瓢',
|
||||
'pie': '撇瞥',
|
||||
'pin': '品拼贫频聘',
|
||||
'ping':'平评凭瓶苹萍乒',
|
||||
'po': '破坡婆迫颇魄',
|
||||
'pou': '剖',
|
||||
'pu': '铺普朴谱浦葡仆',
|
||||
# ---- qi ----
|
||||
'qi': '起期其气七齐器汽奇乞旗棋',
|
||||
'qia': '恰掐洽',
|
||||
'qian':'前钱千签浅欠牵铅迁谦',
|
||||
'qiang':'强抢墙枪腔',
|
||||
'qiao':'桥瞧敲俏巧悄壳',
|
||||
'qie': '且切窃茄',
|
||||
'qin': '亲琴勤侵秦',
|
||||
'qing':'请清轻青情晴顷倾',
|
||||
'qiong':'穷琼',
|
||||
'qiu': '求球秋丘邱',
|
||||
'qu': '去取区曲娶趣趋渠',
|
||||
'quan':'全权圈劝拳泉犬',
|
||||
'que': '却确缺雀鹊',
|
||||
'qun': '群裙',
|
||||
# ---- ran ----
|
||||
'ran': '然染燃',
|
||||
'rang':'让嚷壤',
|
||||
'rao': '绕饶扰',
|
||||
're': '热惹',
|
||||
'ren': '人认任仁忍韧刃',
|
||||
'reng':'仍扔',
|
||||
'ri': '日',
|
||||
'rong':'容荣融绒溶',
|
||||
'rou': '肉柔揉',
|
||||
'ru': '如入辱儒乳汝',
|
||||
'rua': '挼',
|
||||
'ruan':'软',
|
||||
'rui': '瑞锐',
|
||||
'run': '润闰',
|
||||
'ruo': '若弱',
|
||||
# ---- sa ----
|
||||
'sa': '撒洒萨',
|
||||
'sai': '赛塞腮',
|
||||
'san': '三散伞',
|
||||
'sang':'桑嗓丧',
|
||||
'sao': '扫嫂骚',
|
||||
'se': '色涩瑟',
|
||||
'sen': '森',
|
||||
'seng':'僧',
|
||||
'sha': '沙杀傻纱啥刹煞',
|
||||
'shai':'晒筛',
|
||||
'shan':'山善闪衫删扇陕',
|
||||
'shang':'上商伤尚赏裳',
|
||||
'shao':'少烧稍绍捎勺',
|
||||
'she': '社设舍射蛇摄涉奢',
|
||||
'shen':'身深神甚伸什申慎渗',
|
||||
'sheng':'生声省胜圣升盛剩',
|
||||
'shi': '是事时十式市师世史示使士失视实识施',
|
||||
'shou':'手受收首守售寿授瘦',
|
||||
'shu': '书数术树输属舒叔殊疏蔬',
|
||||
'shua':'刷耍',
|
||||
'shuai':'帅摔甩衰',
|
||||
'shuan':'栓拴',
|
||||
'shuang':'双爽',
|
||||
'shui':'水睡税',
|
||||
'shun':'顺瞬',
|
||||
'shuo':'说硕烁',
|
||||
'si': '四死思斯司私似撕寺饲',
|
||||
'song':'送松宋诵颂',
|
||||
'sou': '搜艘嗽',
|
||||
'su': '素速苏诉宿俗塑肃',
|
||||
'suan':'算酸蒜',
|
||||
'sui': '岁随虽碎遂穗',
|
||||
'sun': '孙损笋',
|
||||
'suo': '所锁索缩嗦',
|
||||
# ---- ta ----
|
||||
'ta': '他她它塔踏塌拓',
|
||||
'tai': '太台抬态泰苔',
|
||||
'tan': '谈弹叹探贪摊滩瘫',
|
||||
'tang':'堂糖汤唐躺烫塘',
|
||||
'tao': '讨套逃掏桃掏',
|
||||
'te': '特',
|
||||
'teng':'疼腾藤',
|
||||
'ti': '体题提替踢蹄剃',
|
||||
'tian':'天田甜填添',
|
||||
'tiao':'条跳挑调',
|
||||
'tie': '铁贴帖',
|
||||
'ting':'听停厅挺庭亭艇',
|
||||
'tong':'同通痛统童筒铜桶',
|
||||
'tou': '头投偷透',
|
||||
'tu': '图土突途徒吐兔涂',
|
||||
'tuan':'团',
|
||||
'tui': '推退腿褪',
|
||||
'tun': '吞屯臀',
|
||||
'tuo': '托脱拖妥驼椭',
|
||||
# ---- w ----
|
||||
'wa': '挖瓦蛙娃袜',
|
||||
'wai': '外歪',
|
||||
'wan': '完万晚玩弯碗湾挽',
|
||||
'wang':'王往望网忘旺亡',
|
||||
'wei': '为位未卫维微围委谓违唯尾',
|
||||
'wen': '问文闻温稳吻纹',
|
||||
'weng':'翁嗡瓮',
|
||||
'wo': '我握窝卧沃',
|
||||
'wu': '无五物误务武舞午污伍',
|
||||
# ---- xi ----
|
||||
'xi': '系西习戏喜细吸希洗悉溪膝席惜',
|
||||
'xia': '下夏吓虾峡狭暇霞',
|
||||
'xian':'先现线县显险鲜闲限陷献',
|
||||
'xiang':'想向相像响项香乡箱详',
|
||||
'xiao':'小笑校晓消销肖效萧',
|
||||
'xie': '写些鞋谢协血携斜泄',
|
||||
'xin': '新心信辛欣薪芯',
|
||||
'xing':'行性形星兴型醒幸姓',
|
||||
'xiong':'雄胸凶兄熊',
|
||||
'xiu': '修秀休羞锈袖嗅',
|
||||
'xu': '需须许序续徐虚绪蓄',
|
||||
'xuan':'选宣旋悬玄轩',
|
||||
'xue': '学雪血穴靴',
|
||||
'xun': '寻讯训迅询巡循旬',
|
||||
# ---- ya ----
|
||||
'ya': '压呀牙亚讶雅鸦鸭',
|
||||
'yan': '眼言演烟严沿研盐验颜宴艳延',
|
||||
'yang':'样养洋阳羊央仰扬',
|
||||
'yao': '要药摇腰咬耀邀遥谣',
|
||||
'ye': '也业夜叶爷野冶页',
|
||||
'yi': '一已以意义医衣依易忆疑谊益议',
|
||||
'yin': '因音引银印隐饮阴姻',
|
||||
'ying':'应英影营迎硬赢映鹰',
|
||||
'yo': '哟',
|
||||
'yong':'用永拥勇涌泳庸',
|
||||
'you':'有又由右油游友优幼忧尤呦',
|
||||
'yu': '于与语雨鱼玉育余遇欲愈预域娱',
|
||||
'yuan':'元原员院远愿圆园源冤缘',
|
||||
'yue': '月越约阅跃悦岳',
|
||||
'yun': '云运允匀韵孕',
|
||||
# ---- za ----
|
||||
'za': '杂砸咋',
|
||||
'zai': '在再载咱灾宰',
|
||||
'zan': '咱赞暂攒',
|
||||
'zang':'藏脏葬',
|
||||
'zao': '早造遭糟澡灶燥躁',
|
||||
'ze': '则责择泽',
|
||||
'zei': '贼',
|
||||
'zen': '怎',
|
||||
'zeng':'增赠曾憎',
|
||||
'zha': '炸查扎眨乍榨渣',
|
||||
'zhai':'宅摘窄债斋',
|
||||
'zhan':'战站占展沾粘斩',
|
||||
'zhang':'张章长掌涨帐障丈',
|
||||
'zhao':'找照招着朝召兆',
|
||||
'zhe': '这者着折哲浙',
|
||||
'zhen':'真阵镇针震振诊枕',
|
||||
'zheng':'正整证政争征挣睁郑',
|
||||
'zhi': '只之至制知指直纸志治支止职织质执',
|
||||
'zhong':'中种重众忠终钟仲',
|
||||
'zhou':'周州洲舟昼皱骤轴',
|
||||
'zhu': '主住注助著朱猪竹煮筑祝驻',
|
||||
'zhua':'抓',
|
||||
'zhuai':'拽',
|
||||
'zhuan':'转专赚传砖',
|
||||
'zhuang':'装状撞庄壮',
|
||||
'zhui':'追坠缀',
|
||||
'zhun':'准',
|
||||
'zhuo':'桌捉着灼卓浊',
|
||||
'zi': '子自字资紫仔兹姿咨',
|
||||
'zong':'总宗纵综棕踪',
|
||||
'zou': '走奏揍邹',
|
||||
'zu': '组足祖族租阻卒',
|
||||
'zuan':'钻攥',
|
||||
'zui': '最嘴罪醉',
|
||||
'zun': '尊遵',
|
||||
'zuo': '做作坐座左昨',
|
||||
}
|
||||
|
||||
# 词/短语表:sp=整词拼音串(去声调),hz=UTF-8 词条,w=字频权重(越大越常用)。
|
||||
# 覆盖搜索框/文件名/输入框常见词;多音节词让「短语精确命中 + 前缀整词提示」生效。
|
||||
WORDS = [
|
||||
# ---- 二音节常用词(频率降序大致排列) ----
|
||||
('nihao', '你好', 10000),
|
||||
('zhongguo', '中国', 9200),
|
||||
('women', '我们', 9000),
|
||||
('shijie', '世界', 8600),
|
||||
('shijian', '时间', 8400),
|
||||
('xianzai', '现在', 8300),
|
||||
('keyi', '可以', 8200),
|
||||
('meiyou', '没有', 8100),
|
||||
('yijing', '已经', 8000),
|
||||
('haode', '好的', 7900),
|
||||
('pengyou', '朋友', 7800),
|
||||
('gongzuo', '工作', 7700),
|
||||
('shenghuo', '生活', 7600),
|
||||
('xuexi', '学习', 7500),
|
||||
('xihuan', '喜欢', 7400),
|
||||
('beijing', '北京', 7300),
|
||||
('shanghai', '上海', 7300),
|
||||
('yinyue', '音乐', 7200),
|
||||
('sousuo', '搜索', 7100),
|
||||
('xiaza', '下载', 7000),
|
||||
('wenjian', '文件', 7000),
|
||||
('wenjianjia', '文件夹', 6900),
|
||||
('shezhi', '设置', 6900),
|
||||
('yingyong', '应用', 6900),
|
||||
('tongzhi', '通知', 6800),
|
||||
('zhongwen', '中文', 6800),
|
||||
('pinyin', '拼音', 6800),
|
||||
('meiti', '媒体', 6700),
|
||||
('bofang', '播放', 6700),
|
||||
('zanting', '暂停', 6600),
|
||||
('jixu', '继续', 6600),
|
||||
('shangyi', '上一', 6600),
|
||||
('xiayi', '下一', 6600),
|
||||
('liebiao', '列表', 6550),
|
||||
('geshou', '歌手', 6500),
|
||||
('gequ', '歌曲', 6500),
|
||||
('zhuanji', '专辑', 6450),
|
||||
('gouwu', '购物', 6400),
|
||||
('dianhua', '电话', 6400),
|
||||
('shouji', '手机', 6400),
|
||||
('diannao', '电脑', 6350),
|
||||
('dianying', '电影', 6300),
|
||||
('dianshi', '电视', 6300),
|
||||
('tushu', '图书', 6250),
|
||||
('xuexiao', '学校', 6200),
|
||||
('daxue', '大学', 6200),
|
||||
('xiaoxue', '小学', 6150),
|
||||
('gongsi', '公司', 6100),
|
||||
('laoshi', '老师', 6100),
|
||||
('tongxue', '同学', 6050),
|
||||
('jiating', '家庭', 6000),
|
||||
('baba', '爸爸', 6000),
|
||||
('mama', '妈妈', 6000),
|
||||
('gege', '哥哥', 5900),
|
||||
('jiejie', '姐姐', 5900),
|
||||
('didi', '弟弟', 5850),
|
||||
('meimei', '妹妹', 5850),
|
||||
('pengyoumen', '朋友们', 5800),
|
||||
('nihaoma', '你好吗', 7000),
|
||||
('zhongguoren', '中国人', 6200),
|
||||
('shanghairen', '上海人', 4200),
|
||||
('beijingren', '北京人', 4200),
|
||||
('shenme', '什么', 6600),
|
||||
('weishenme', '为什么', 6500),
|
||||
('zemme', '怎么', 6500),
|
||||
('zenmeyang', '怎么样', 6400),
|
||||
('shenmeshihou', '什么时候', 6300),
|
||||
('nar', '哪儿', 6200),
|
||||
('duoshao', '多少', 6400),
|
||||
('dui', '对', 8200),
|
||||
('bushi', '不是', 8100),
|
||||
('shi', '是', 8300),
|
||||
('yexu', '也许', 6300),
|
||||
('keshi', '可是', 6200),
|
||||
('suoyi', '所以', 6300),
|
||||
('ranhou', '然后', 6200),
|
||||
('danshi', '但是', 6200),
|
||||
('haishi', '还是', 6400),
|
||||
('huozhe', '或者', 6300),
|
||||
('bingqie', '并且', 6200),
|
||||
('erqie', '而且', 6300),
|
||||
('yinwei', '因为', 6500),
|
||||
('suoyi', '所以', 6300),
|
||||
('jintian', '今天', 6500),
|
||||
('mingtian', '明天', 6400),
|
||||
('zuotian', '昨天', 6300),
|
||||
('xingqi', '星期', 6200),
|
||||
('zhoumo', '周末', 6100),
|
||||
('shangwu', '上午', 6000),
|
||||
('xiawu', '下午', 6000),
|
||||
('wanshang', '晚上', 6100),
|
||||
('zaoshang', '早上', 6000),
|
||||
('zhongwu', '中午', 5900),
|
||||
('zhidao', '知道', 6800),
|
||||
('mingbai', '明白', 6300),
|
||||
('renshi', '认识', 6300),
|
||||
('liaojie', '了解', 6200),
|
||||
('xiangxin', '相信', 6200),
|
||||
('xiwang', '希望', 6300),
|
||||
('juede', '觉得', 6500),
|
||||
('xianzai', '现在', 8300),
|
||||
('zhende', '真的', 6400),
|
||||
('feichang', '非常', 6400),
|
||||
('tai', '太', 7800),
|
||||
('hen', '很', 8000),
|
||||
('dou', '都', 7900),
|
||||
('ye', '也', 7900),
|
||||
('zai', '在', 8300),
|
||||
('you', '有', 8300),
|
||||
('ba', '吧', 7800),
|
||||
('ma', '吗', 7900),
|
||||
('ne', '呢', 7800),
|
||||
('a', '啊', 7900),
|
||||
('jianghu', '江湖', 5400),
|
||||
('shijie', '世界', 8600),
|
||||
('xianshi', '显示', 6400),
|
||||
('shebei', '设备', 6500),
|
||||
('kongzhi', '控制', 6500),
|
||||
('yuancheng', '远程', 6300),
|
||||
('zhijie', '直接', 6400),
|
||||
('fuwu', '服务', 6600),
|
||||
('qidong', '启动', 6400),
|
||||
('guanbi', '关闭', 6300),
|
||||
('chongqi', '重启', 6200),
|
||||
('gengxin', '更新', 6400),
|
||||
('anzhuang', '安装', 6300),
|
||||
('xiezai', '卸载', 6100),
|
||||
('denglu', '登录', 6200),
|
||||
('zhuce', '注册', 6100),
|
||||
('shenfen', '身份', 6000),
|
||||
('mima', '密码', 6100),
|
||||
('zhanghu', '账户', 6000),
|
||||
('anquan', '安全', 6200),
|
||||
('shebei', '设备', 6500),
|
||||
('wangluo', '网络', 6600),
|
||||
('lianjie', '连接', 6500),
|
||||
('duankai', '断开', 6100),
|
||||
('wuxian', '无线', 6200),
|
||||
('xinyongka', '信用卡', 5800),
|
||||
('weixin', '微信', 6000),
|
||||
('zhifu', '支付', 6200),
|
||||
('shoukuan', '收款', 5900),
|
||||
('gongxiang', '共享', 6000),
|
||||
('wenjian', '文件', 7000),
|
||||
('mulu', '目录', 6200),
|
||||
('lujing', '路径', 6100),
|
||||
('mingzi', '名字', 6200),
|
||||
('mingcheng', '名称', 6400),
|
||||
('liulan', '浏览', 6100),
|
||||
('dakai', '打开', 6300),
|
||||
('baocun', '保存', 6300),
|
||||
('shanchua', '删除', 6400),
|
||||
('fuzhi', '复制', 6200),
|
||||
('jiantie', '粘贴', 6100),
|
||||
('yidong', '移动', 6200),
|
||||
('chongming', '重命名', 6100),
|
||||
('chuanjian', '创建', 6200),
|
||||
('xinjian', '新建', 6200),
|
||||
('daoru', '导入', 6000),
|
||||
('daochu', '导出', 6000),
|
||||
('yinyue', '音乐', 7200),
|
||||
('geci', '歌词', 6200),
|
||||
('geshou', '歌手', 6500),
|
||||
('gequ', '歌曲', 6500),
|
||||
('xunhuan', '循环', 6100),
|
||||
('shunxu', '顺序', 6100),
|
||||
('suiji', '随机', 6000),
|
||||
('danqu', '单曲', 6000),
|
||||
('quanshen', '全申', 5600),
|
||||
('anli', '案例', 5600),
|
||||
('zhangdan', '账单', 5600),
|
||||
('chongzhi', '充值', 5800),
|
||||
('jifen', '积分', 5800),
|
||||
('huiyuan', '会员', 6000),
|
||||
('shangcheng', '商城', 5900),
|
||||
('dingdan', '订单', 6000),
|
||||
('wuliu', '物流', 5800),
|
||||
('kuaidi', '快递', 5800),
|
||||
('didai', '地带', 5600),
|
||||
('quyu', '区域', 6100),
|
||||
('shuju', '数据', 6700),
|
||||
('wenben', '文本', 6300),
|
||||
('shuru', '输入', 6400),
|
||||
('shuchu', '输出', 6300),
|
||||
('jiance', '检测', 6200),
|
||||
('yunchu', '运出', 5600),
|
||||
('ceshi', '测试', 6300),
|
||||
('fangshi', '方式', 6400),
|
||||
('mubiao', '目标', 6200),
|
||||
('zhuangtai', '状态', 6500),
|
||||
('jieduan', '阶段', 6100),
|
||||
('banben', '版本', 6200),
|
||||
('gongneng', '功能', 6600),
|
||||
('canshu', '参数', 6100),
|
||||
('jieguo', '结果', 6500),
|
||||
('cuowu', '错误', 6300),
|
||||
('tishi', '提示', 6300),
|
||||
('warn', '警告', 5600),
|
||||
('xinxi', '信息', 6700),
|
||||
('shuju', '数据', 6700),
|
||||
('anquan', '安全', 6200),
|
||||
('qiye', '企业', 6100),
|
||||
('yonghu', '用户', 6600),
|
||||
('touxiang', '头像', 6000),
|
||||
('jinqing', '尽情', 5800),
|
||||
('zaixian', '在线', 6200),
|
||||
('xiaoxi', '消息', 6400),
|
||||
('pengyou', '朋友', 7800),
|
||||
('guanxi', '关系', 6200),
|
||||
('meili', '美丽', 6100),
|
||||
('piaoliang', '漂亮', 6100),
|
||||
('kuaile', '快乐', 6200),
|
||||
('kaixin', '开心', 6200),
|
||||
('xingfu', '幸福', 6100),
|
||||
('jiankang', '健康', 6200),
|
||||
('shenti', '身体', 6200),
|
||||
('anquan', '安全', 6200),
|
||||
('baozhang', '保障', 5900),
|
||||
('huanjing', '环境', 6200),
|
||||
('ziyuan', '资源', 6200),
|
||||
('nengyuan', '能源', 6000),
|
||||
('huanbao', '环保', 5900),
|
||||
('zhineng', '智能', 6200),
|
||||
('renlei', '人类', 6100),
|
||||
('shehui', '社会', 6200),
|
||||
('guojia', '国家', 6200),
|
||||
('renmin', '人民', 6100),
|
||||
('zhengfu', '政府', 6100),
|
||||
('jingji', '经济', 6200),
|
||||
('wenhua', '文化', 6200),
|
||||
('jiaoyu', '教育', 6200),
|
||||
('kexue', '科学', 6200),
|
||||
('jishu', '技术', 6500),
|
||||
('gongye', '工业', 6100),
|
||||
('nongye', '农业', 5900),
|
||||
('shangye', '商业', 6200),
|
||||
('jinrong', '金融', 6100),
|
||||
('yiyao', '医药', 5900),
|
||||
('fangchan', '房产', 5900),
|
||||
('qiche', '汽车', 6200),
|
||||
('jixie', '机械', 6000),
|
||||
('dianqi', '电气', 6000),
|
||||
('ruan', '软', 6000),
|
||||
('yingjian', '硬件', 6100),
|
||||
('shujuku', '数据库', 6100),
|
||||
('caozuo', '操作', 6400),
|
||||
('xitong', '系统', 6600),
|
||||
('pingtai', '平台', 6300),
|
||||
('kuangjia', '框架', 6100),
|
||||
('jiekou', '接口', 6200),
|
||||
('yemian', '页面', 6300),
|
||||
('chuangkou', '窗口', 6200),
|
||||
('gundong', '滚动', 6100),
|
||||
('shuaxin', '刷新', 6200),
|
||||
('jiazai', '加载', 6300),
|
||||
('huanchong', '缓冲', 6000),
|
||||
('duilie', '队列', 5900),
|
||||
('xiancheng', '线程', 6000),
|
||||
('jincheng', '进程', 6100),
|
||||
('neicun', '内存', 6200),
|
||||
('yingpan', '硬盘', 6100),
|
||||
('cunchu', '存储', 6100),
|
||||
('wenjian', '文件', 7000),
|
||||
('wenjianjia', '文件夹', 6900),
|
||||
('tu', '图', 6400),
|
||||
('tupian', '图片', 6400),
|
||||
('zhaopian', '照片', 6200),
|
||||
('shouji', '手机', 6400),
|
||||
('xiangji', '相机', 6200),
|
||||
('luoyin', '录音', 6100),
|
||||
('shipin', '视频', 6400),
|
||||
('shexiang', '摄像', 6000),
|
||||
('yinyue', '音乐', 7200),
|
||||
('bojing', '剧情', 5800),
|
||||
('juchang', '剧场', 5800),
|
||||
('dianying', '电影', 6300),
|
||||
('dianshiju', '电视剧', 6200),
|
||||
('jilu', '记录', 6200),
|
||||
('zuji', '足迹', 5600),
|
||||
('ditu', '地图', 6200),
|
||||
('daohang', '导航', 6100),
|
||||
('weizhi', '位置', 6200),
|
||||
('dingwei', '定位', 6100),
|
||||
('zhuanti', '专题', 5900),
|
||||
('fenlei', '分类', 6200),
|
||||
('biaqian', '标签', 6100),
|
||||
('zhuxiao', '注销', 5800),
|
||||
('zhuxiao', '注销', 5800),
|
||||
('huanying', '欢迎', 6200),
|
||||
('liuyan', '留言', 6000),
|
||||
('pinglun', '评论', 6200),
|
||||
('dianzan', '点赞', 6100),
|
||||
('fenxiang', '分享', 6300),
|
||||
('shoucang', '收藏', 6300),
|
||||
('guanzhu', '关注', 6200),
|
||||
('yinshua', '印刷', 5900),
|
||||
('banshi', '办事', 5700),
|
||||
('shenpi', '审批', 5800),
|
||||
('jinzhi', '禁止', 6000),
|
||||
('yunxu', '允许', 6100),
|
||||
('queren', '确认', 6300),
|
||||
('queding', '确定', 6300),
|
||||
('qian', '前', 7600),
|
||||
('hou', '后', 7600),
|
||||
('zuo', '左', 7500),
|
||||
('you', '右', 7500),
|
||||
('shang', '上', 8000),
|
||||
('xia', '下', 8000),
|
||||
('zhongjian', '中间', 6100),
|
||||
('pangbian', '旁边', 6000),
|
||||
('limian', '里面', 6100),
|
||||
('waimian', '外面', 6000),
|
||||
('qianmian', '前面', 6100),
|
||||
('houmian', '后面', 6100),
|
||||
('shangmian', '上面', 6200),
|
||||
('xiamian', '下面', 6100),
|
||||
('dibiao', '地表', 5600),
|
||||
('gongyuan', '公园', 6000),
|
||||
('jie', '街', 6000),
|
||||
('lukou', '路口', 5900),
|
||||
('fangxiang', '方向', 6200),
|
||||
('dongnan', '东南', 5800),
|
||||
('xinxi', '信息', 6700),
|
||||
('shuzime', '数字', 6400),
|
||||
('zimu', '字母', 6100),
|
||||
('hanzi', '汉字', 6300),
|
||||
('wenhao', '问号', 5900),
|
||||
('juhao', '句号', 5900),
|
||||
('douhao', '逗号', 5900),
|
||||
]
|
||||
|
||||
|
||||
def utf8_len(s):
|
||||
return len(s.encode('utf-8'))
|
||||
|
||||
|
||||
def main():
|
||||
lines = []
|
||||
lines.append('/* 自动生成 by gen_pinyin_lex.py —— 中文拼音→候选字/词 频序表(去声调,UTF-8,.rodata)。*/')
|
||||
lines.append('/* 词库规模:音节 %d 条 + 词语 %d 条;候选字全部落在 GB2312 常用字集(common/gbk tier)。 */' % (len(SYL), len(WORDS)))
|
||||
lines.append('#ifndef MEOW_PINYIN_LEX_H')
|
||||
lines.append('#define MEOW_PINYIN_LEX_H')
|
||||
lines.append('#include <stdint.h>')
|
||||
lines.append('')
|
||||
lines.append('typedef struct { const char *py; const char *hz; } py_syl;')
|
||||
lines.append('typedef struct { const char *sp; const char *hz; int w; } py_word;')
|
||||
lines.append('')
|
||||
lines.append('#define SYL_N %d' % len(SYL))
|
||||
lines.append('static const py_syl SYL[SYL_N] = {')
|
||||
for k, v in SYL.items():
|
||||
lines.append(' {"%s", "%s"},' % (k, v))
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
lines.append('#define WORD_N %d' % len(WORDS))
|
||||
lines.append('static const py_word WORDS[WORD_N] = {')
|
||||
for sp, hz, w in WORDS:
|
||||
lines.append(' {"%s","%s",%d},' % (sp, hz, w))
|
||||
lines.append('};')
|
||||
lines.append('')
|
||||
lines.append('#endif /* MEOW_PINYIN_LEX_H */')
|
||||
with open('meow_pinyin_lex.h', 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
|
||||
# ---- 体积账(与设计稿 §2.3 同口径:结构体 + 字符串,含 NUL)----
|
||||
sz_syl_struct = 8 * len(SYL) # py ptr(4) + hz ptr(4)
|
||||
sz_syl_str = sum(utf8_len(k) + 1 + utf8_len(v) + 1 for k, v in SYL.items())
|
||||
sz_word_struct = 12 * len(WORDS) # sp ptr(4) + hz ptr(4) + w(4)
|
||||
sz_word_str = sum(utf8_len(sp) + 1 + utf8_len(hz) + 1 for sp, hz, w in WORDS)
|
||||
total = sz_syl_struct + sz_syl_str + sz_word_struct + sz_word_str
|
||||
print('--- 词库体积账 ---')
|
||||
print('音节表: %d 条, 结构体 %d B + 字符串 %d B = %d B' % (len(SYL), sz_syl_struct, sz_syl_str, sz_syl_struct + sz_syl_str))
|
||||
print('词语表: %d 条, 结构体 %d B + 字符串 %d B = %d B' % (len(WORDS), sz_word_struct, sz_word_str, sz_word_struct + sz_word_str))
|
||||
print('合计 .rodata 估算: %d B (%.1f KiB)' % (total, total / 1024.0))
|
||||
print('--- 出错/重复检测 ---')
|
||||
# 词表允许同键不同条目(如 'wei' 等),这里仅打印去重提示
|
||||
print('生成完成 meow_pinyin_lex.h')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* meow_ui_activity.h — SDK 控件生命周期与归属(§6.1):Activity 栈 + 三层控件模型。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(activity 模块)。根治「跨页面控件残留」:控件命中/绘制必须严格
|
||||
* 绑定其所属页面。三层职责(App→系统):①页面控件(绑定 owner Activity);②App 跨 Activity
|
||||
* 控件(SDK 托管骨架);③系统级控件(状态栏/通知中心/返回滑条/电源菜单,仅框架内部,无公共 API)。
|
||||
*/
|
||||
#ifndef MEOW_UI_ACTIVITY_H
|
||||
#define MEOW_UI_ACTIVITY_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §6.1 控件生命周期与归属(Activity 栈 + 三层控件模型) [源:2026-08-30] */
|
||||
/* ============================================================ */
|
||||
/* 根治「跨页面控件残留」:控件命中/绘制必须严格绑定其所属页面,否则会出现
|
||||
* "按钮已不可见但仍被触摸命中"(如全屏页触摸到主页按钮位置闪现 accent 底)。
|
||||
*
|
||||
* 三层职责(从 App 到系统,风险从低到高):
|
||||
* ① 页面控件:归属唯一 owner Activity;命中/实时按压只在「当前 active Activity
|
||||
* 的控件集」里生效。对象不销毁(同 Activity 内复用),弹栈回来自动恢复 →
|
||||
* 回页面零重建。
|
||||
* ② App 跨 Activity 控件(SDK 托管):钉在 App 的 Activity 栈之上、跨 Activity 常驻,
|
||||
* 逻辑/渲染由 SDK 调度。本轮为「可用骨架」(见上 API 注释),待 B2 细化。
|
||||
* ③ 系统级控件(状态栏/通知中心/返回滑条/电源菜单):仅框架内部,无公共 API(不开放)。
|
||||
*
|
||||
* 触摸命中分流顺序:③ 系统全局 → ② App 全局 → ① 当前 active Activity 页面控件;
|
||||
* 某层命中即消费,否则落到下一层。 */
|
||||
|
||||
/* ① Activity 栈。控件创建时绑定「当前 active Activity」为 owner;弹栈回来自动恢复。
|
||||
* 栈空时 active() 返回 0(根/default Activity),故未显式管理栈的 App 默认所有控件
|
||||
* 归属根 Activity 0。push 新 Activity(进子页/全屏)、pop 回上级(回根)。 */
|
||||
void meow_ui_activity_push(int id);
|
||||
void meow_ui_activity_pop(void);
|
||||
int meow_ui_activity_active(void); /* 当前 active activity id(栈空=0) */
|
||||
int meow_ui_activity_depth(void); /* 当前栈深(push/pop 页数;供 M3 导航上报用) */
|
||||
|
||||
/* ② App 跨 Activity 控件(SDK 托管,骨架)。SDK 维护全局控件列表:
|
||||
* - 渲染:每帧(有脏区需上屏时,meow_ui_gfx present())在 active activity 内容之上、
|
||||
* 全局状态栏之下调用 render_cb(跨 Activity 常驻);
|
||||
* - 触摸:TAP 先给 global hit_cb(返回 1=消费),否则落到 active activity 页面控件。
|
||||
* 逻辑/渲染由 SDK 调度,App 只需声明。注:骨架,待 B2 细化。 */
|
||||
typedef void (*meow_global_render_fn)(void *ctx); /* 每次渲染帧回调(SDK 调用) */
|
||||
typedef int (*meow_global_hit_fn)(void *ctx, int x, int y); /* 命中返回1=消费 */
|
||||
void meow_ui_global_add(int id, meow_global_render_fn r, meow_global_hit_fn h, void *ctx);
|
||||
void meow_ui_global_remove(int id);
|
||||
|
||||
/* ③ 系统级控件:状态栏/通知中心/左缘返回滑条/电源菜单等全局叠加层——仅 SDK 内部接口,
|
||||
* 不向用户 App 开放任何公共悬浮/叠加层 API。既有状态栏/返回滑条逻辑(§3.1)不动。 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_ACTIVITY_H */
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* meow_ui_audio.h — SDK 独占音频客户端(§11,devdocs/plan/AUDIOD_DESIGN.md §7;meow_ui_audio.c)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(audio 模块)。App 只经本 API 喂 PCM 给 meow_audiod
|
||||
* (独占 hw:0,0 守护,svcd 托管)。audiod 不在一律失败(open 返回 MEOW_ERR_NOENT,负值),
|
||||
* 不做隐式直开回退;调试可用 env MEOW_AUDIO_DIRECT=1 走自带直接 ALSA 直开路径(非正式能力)。
|
||||
*/
|
||||
#ifndef MEOW_UI_AUDIO_H
|
||||
#define MEOW_UI_AUDIO_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §11 独占音频客户端(devdocs/plan/AUDIOD_DESIGN.md §7;meow_ui_audio.c) */
|
||||
/* ============================================================ */
|
||||
/* 说明:App 只经本 API 喂 PCM 给 meow_audiod(独占 hw:0,0 守护,svcd 托管)。audiod 不在一律
|
||||
* 失败(open 返回 MEOW_ERR_NOENT,负值),不做隐式直开回退;调试可用 env MEOW_AUDIO_DIRECT=1 走
|
||||
* 自带直接 ALSA 直开路径(非正式能力,勿依赖)。
|
||||
* 生命周期:open 取 stream_id>0 → write 喂 PCM(阻塞,socket 背压)→ pause/resume → close 释放。
|
||||
* 被抢占(后开流赢):SDK 内部读线程收 MSG_AUDIO_PREEMPTED → 调 on_preempt 回调 + 标写死
|
||||
* (后续 write 返回负值,App 应重新 open)。 */
|
||||
|
||||
/* 当前流快照(meow_audio_query 填充)。 */
|
||||
typedef struct meow_audio_state {
|
||||
int active; /* 1=audiod 有活跃流(不一定是本 App 的) */
|
||||
char app_id[32]; /* 活跃流 app_id(无流为 "-") */
|
||||
uint32_t stream_id; /* 活跃流 id */
|
||||
uint32_t rate; /* 活跃流采样率 */
|
||||
uint16_t ch; /* 活跃流声道数 */
|
||||
uint16_t fmt; /* 活跃流格式(AUDIO_FMT_*) */
|
||||
uint64_t written_frames; /* 客户端写入 ring 的帧 */
|
||||
uint64_t played_frames; /* 播放线程写到设备的帧 */
|
||||
uint32_t underrun; /* xrun 计数 */
|
||||
int paused; /* 当前流是否暂停 */
|
||||
int route_port; /* 当前路由口(-1 未知) */
|
||||
int vol; /* 当前音量 0-100 */
|
||||
} meow_audio_state;
|
||||
|
||||
/* 被抢占回调:by_app=抢占者 app_id(非空)。ctx 由 on_preempt 注册时传入。 */
|
||||
typedef void (*meow_audio_preempt_fn)(const char *by_app, void *ctx);
|
||||
|
||||
/* 开流。rate/ch/fmt=App 声源原生参数(fmt 值见 meow_ipc.h AUDIO_FMT_*)。
|
||||
* 返回 stream_id(>0);负错误码(MEOW_ERR_NOENT=audiod 不在;MEOW_ERR_INVAL=参数非法;
|
||||
* MEOW_ERR_NODEV=无可用 PCM 设备;MEOW_ERR_* 常量本身即负值)。open 后客户端须按内部记录的
|
||||
* grant 格式产出 PCM。 */
|
||||
int meow_audio_open(int rate, int ch, int fmt);
|
||||
/* 写 PCM。pcm=native fmt/ch 样本(open 传的参数),frames=帧数;内部归一成 grant 并分批发送。
|
||||
* 返回写入帧数;<0=流已死(被抢占/audiod 断开),App 应重新 open。阻塞(socket 背压)。 */
|
||||
int meow_audio_write(const void *pcm, int frames);
|
||||
/* 暂停(1)/恢复(0)当前流。返回 0 成功;<0 失败。 */
|
||||
int meow_audio_pause(int sid, int paused);
|
||||
/* 关流(STOP + 释放)。返回 0;连接保留供下次 open。 */
|
||||
int meow_audio_close(int sid);
|
||||
/* 查询 audiod 快照(填充 out)。返回 0 成功;<0 失败。 */
|
||||
int meow_audio_query(meow_audio_state *out);
|
||||
/* 注册被抢占回调(可选;内部读线程分发)。重复调用幂等。 */
|
||||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_AUDIO_H */
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* meow_ui_config.h — SDK 应用配置读写(§9.2,meow_config_*,M2.x 最小可用版)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(config 模块)。App 自有配置读写:与系统设置 meow_settings_*
|
||||
* (板载 settings.conf、系统级、带 schema)明确区分——这是单个 App 自己的键值偏好,
|
||||
* 落 TF 数据目录,不占板载、无 schema。返回码 MEOW_CFG_* 见下。
|
||||
*/
|
||||
#ifndef MEOW_UI_CONFIG_H
|
||||
#define MEOW_UI_CONFIG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* meow_config_*(§9.2)专用返回码。数值与 MEOWOS_API.md §1 的同名错误码一致,
|
||||
* 但用 meow_cfg 前缀——因为 MEOW_OK/MEOW_ERR_* 这个命名空间已被共享协议头
|
||||
* meow_ipc.h(枚举)与 launcher meow_settings.h(宏)占用,SDK 的 meow_notify_ipc.c
|
||||
* 同 TU include meow_ui.h + meow_ipc.h,若在此定义同名宏会导致「宏展开枚举标识符」冲突。 */
|
||||
#define MEOW_CFG_OK 0 /* 成功 */
|
||||
#define MEOW_CFG_ERR_INVAL -1 /* 参数/取值非法(空 key、NULL out、outsz<=0 等) */
|
||||
#define MEOW_CFG_ERR_NOENT -2 /* 键未设/文件不存在 */
|
||||
#define MEOW_CFG_ERR_IO -5 /* 读写(存储/文件)失败 */
|
||||
#define MEOW_CFG_ERR_NOMEM -6 /* 路径超长 */
|
||||
#define MEOW_CFG_ERR_NODEV -10 /* 无数据目录(卡不在/只读/不可建) */
|
||||
|
||||
/* ============================================================ */
|
||||
/* §9.2 应用配置读写(meow_config_*,M2.x 最小可用版) */
|
||||
/* ============================================================ */
|
||||
/* App 自有配置读写:与系统设置 meow_settings_*(板载 settings.conf、系统级、带 schema)
|
||||
* 明确区分——这是单个 App 自己的键值偏好,落 TF 数据目录,不占板载、无 schema。
|
||||
*
|
||||
* - 数据目录:优先 env MEOW_DATA_DIR(launcher 注入 /usr/data/mnt/sd_0/MeowData/<pkg>/data,
|
||||
* 见 app_launcher.c launch_app);无 env(手动跑/宿主测/无卡)回退 /tmp/meowcfg_<pid>
|
||||
* (非持久但仍可写)。meow_config_dir() 返回最终目录;空/不可建返回 NULL。
|
||||
* - 文件布局:每键一小文件 <data>/<sanitized_key>.cfg,内容=值字符串(空串=0 字节文件)。
|
||||
* key 只允许 [A-Za-z0-9._-],其余字符替换为 '_'(杜绝路径穿越);key 上限 64。
|
||||
* - 轻量:每次 get 读文件(App 配置小),不做大内存缓存;pthread_once 惰性初始化目录。
|
||||
* - 失败降级:无数据目录/写失败 → 返回负错误码(见上 MEOW_CFG_ERR_*),不崩不阻塞。
|
||||
*
|
||||
* 返回码:MEOW_CFG_OK(0) 成功;(set) 负=失败;(get) MEOW_CFG_ERR_NOENT(-2)=键未设/文件不存在;
|
||||
* meow_config_int/bool 对任何非 MEOW_CFG_OK 一律返回 def。
|
||||
* 错误码数值同 MEOWOS_API.md §1 / meow_settings_*(MEOW_ERR_*)一致,便于 App 习惯。 */
|
||||
const char *meow_config_dir(void); /* 当前 App 数据目录(data);空/不可建 NULL */
|
||||
int meow_config_set(const char *key, const char *val);/* 写 <data>/<key>.cfg;成功 MEOW_CFG_OK(0) */
|
||||
int meow_config_get(const char *key, char *out, int outsz); /* 读;未设/不存在返回 MEOW_CFG_ERR_NOENT */
|
||||
int meow_config_int(const char *key, int def); /* 便捷 int;未设/非法返回 def */
|
||||
int meow_config_bool(const char *key, int def); /* 便捷 bool;未设/非法返回 def */
|
||||
void meow_config_remove(const char *key); /* 删键(无该键/无目录则静默) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_CONFIG_H */
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* meow_ui_dialog.h — SDK 模态(App 层,非系统叠加):Dialog / BottomSheet(§5.8.6)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(dialog 模块)。复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板
|
||||
* (与 launcher s_dd_modal 同构),控件化。阻塞式(内层 meow_input_pop 轮询,与
|
||||
* meow_ui_text_input 同构);打开前排空残留触摸。关闭途径:点卡片内选项=确认、点卡片外=取消、
|
||||
* 左缘返回手势=取消。不压状态栏(y 钳制)。
|
||||
*/
|
||||
#ifndef MEOW_UI_DIALOG_H
|
||||
#define MEOW_UI_DIALOG_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
#include "meow_ui_ime.h" /* meow_ime_ctx(Dialog INPUT 型) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.8.6 模态(App 层,非系统叠加):Dialog / BottomSheet
|
||||
* 复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板(与 launcher s_dd_modal 同构),控件化。
|
||||
* 阻塞式(内层 meow_input_pop 轮询,与 meow_ui_text_input 同构);打开前排空残留触摸。
|
||||
* 关闭途径:点卡片内选项=确认、点卡片外=取消、左缘返回手势=取消。不压状态栏(y 钳制)。 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* ---- Dialog(弹窗:确认/选择/输入/信息)---- */
|
||||
typedef enum { MEOW_DLG_CONFIRM, MEOW_DLG_SELECT, MEOW_DLG_INPUT, MEOW_DLG_INFO } meow_dlg_t;
|
||||
typedef struct meow_dialog meow_dialog;
|
||||
meow_dialog *meow_dialog_create(meow_dlg_t t, const char *title);
|
||||
void meow_dialog_set_items(meow_dialog *d, const char *const *item, int n, int cur); /* SELECT 型 */
|
||||
void meow_dialog_set_ime(meow_dialog *d, meow_ime_ctx *ime); /* INPUT 型 */
|
||||
void meow_dialog_show(meow_dialog *d); /* 阻塞到确认/取消 */
|
||||
int meow_dialog_result(meow_dialog *d); /* SELECT=选项下标;CONFIRM/INFO 确认=1、取消=0 */
|
||||
void meow_dialog_close(meow_dialog *d); /* 程序化关闭(App 亦可驱动) */
|
||||
void meow_dialog_free(meow_dialog *d);
|
||||
/* CONFIRM 型便捷:点「确定」返回 1,取消/遮罩=0 */
|
||||
int meow_dialog_confirm(meow_dialog *d);
|
||||
|
||||
/* ---- BottomSheet(底部面板:标题 + 选项列表;open 阻塞返回选中 idx/-1)---- */
|
||||
typedef struct meow_sheet meow_sheet;
|
||||
meow_sheet *meow_sheet_create(const char *title, int h);
|
||||
int meow_sheet_add_item(meow_sheet *s, const char *label); /* 返回项下标 */
|
||||
int meow_sheet_open(meow_sheet *s); /* 阻塞;返回选中 idx / -1=取消 */
|
||||
void meow_sheet_free(meow_sheet *s);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_DIALOG_H */
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* meow_ui_draw.h — SDK 绘制层(§3)+ 动画/时间(§7)+ 显示刷新率调度(§7.1)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(draw/gfx 模块)。坐标 0,0 = 左上;颜色均 24-bit RGB
|
||||
* (0xRRGGBB),内部转 RGB565;所有绘制先进离屏 back_buf,调用 meow_ui_flush()/
|
||||
* flush_region() 才上屏。文本字号:28(标题/状态栏)、20(正文/按钮)。
|
||||
*/
|
||||
#ifndef MEOW_UI_DRAW_H
|
||||
#define MEOW_UI_DRAW_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §3 绘制层 */
|
||||
/* ============================================================ */
|
||||
/* init 选项。width/height 为 0 表示自动读取 /dev/fb0 几何;
|
||||
* 无 fb(宿主/qemu 离屏)时用 480×720 缺省([D1])。 */
|
||||
typedef struct meow_ui_opts {
|
||||
int width;
|
||||
int height;
|
||||
} meow_ui_opts;
|
||||
|
||||
int meow_ui_init(const meow_ui_opts *opts); /* 开 fb、读几何、载字库、建 back_buf;0=成功 */
|
||||
void meow_ui_shutdown(void);
|
||||
|
||||
int meow_ui_width(void); /* 480 */
|
||||
int meow_ui_height(void); /* 720 */
|
||||
|
||||
void meow_ui_clear(uint32_t color); /* 整屏清(填背景色) */
|
||||
void meow_ui_put_px(int x, int y, uint32_t color);
|
||||
void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color);
|
||||
void meow_ui_draw_rect(int x, int y, int w, int h, uint32_t color); /* 空心框 */
|
||||
void meow_ui_draw_rect_round(int x, int y, int w, int h, int r, uint32_t color); /* 圆角框 */
|
||||
|
||||
/* 文本(size=20/28) */
|
||||
void meow_ui_text(int x, int y, const char *utf8, int size, uint32_t color);
|
||||
void meow_ui_text_right(int right_x, int y, const char *utf8, int size, uint32_t color);
|
||||
void meow_ui_text_center(int cx, int y, const char *utf8, int size, uint32_t color);
|
||||
int meow_ui_text_width(const char *utf8, int size);
|
||||
int meow_ui_text_height(int size); /* = meow_font_line_height */
|
||||
|
||||
/* 上屏:vsync + 原子 blit 脏区(内建脏区追踪) */
|
||||
void meow_ui_flush(void);
|
||||
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 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* §7 动画/时间(B1 最小可用) */
|
||||
/* ============================================================ */
|
||||
uint64_t meow_ui_time_ms(void); /* 单调毫秒时间戳 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* §7.1 显示刷新率调度(A-1/A-2:从 launcher M2.16 迁入,归口 SDK,PERF_SCHEDULER 显示子域) */
|
||||
/* ============================================================ */
|
||||
/* 设目标档位 60/30/15(launcher 与 app 统一走 SDK 的 present 帧率门控一致)。
|
||||
* 同时把当前有效档对齐到目标(与 launcher settings_load 语义一致)。 */
|
||||
void meow_ui_set_refresh_rate(int r);
|
||||
|
||||
/* 1=恒 max 不降档(性能优先);0=允许 governor 在 [15,30,60] 自适应降/升档。 */
|
||||
void meow_ui_set_refresh_always_max(int on);
|
||||
|
||||
/* 当前有效帧率(governor 可能已降档;诊断用)。 */
|
||||
int meow_ui_effective_rate(void);
|
||||
|
||||
/* 已呈现帧计数(验证 present 频率用)。 */
|
||||
unsigned long long meow_ui_frames_presented(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_DRAW_H */
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* meow_ui_font.h — SDK 字体:应用可读 TF 卡字体列表(§5.7)+ 字体分层/大字库(§10)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(font 模块)。字库权威列表 = launcher 扫描落盘的持久文件
|
||||
* MeowOS/config/fonts/index(CSV);App 只经本接口读该列表。字体「档位」由 launcher 按
|
||||
* app.json 声明的 "fonts:<档>" 注入 MEOW_FONT_TIER;SDK 据档选库(meow_font_reload_tier)。
|
||||
*/
|
||||
#ifndef MEOW_UI_FONT_H
|
||||
#define MEOW_UI_FONT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.7 应用可读 TF 卡字体列表(M2.58:字体索引 / 查列表 / 丢失降级默认) */
|
||||
/* ============================================================ */
|
||||
/* 系统字体管理器的面向应用接口(规格 §5.7.1/§5.7.2):
|
||||
* - 权威列表 = launcher 扫描 MeowOS/fonts/<family>/<weight>/ 后落盘的持久文件
|
||||
* MeowOS/config/fonts/index(CSV:family,weight,path,label,tier,glyph_count)。
|
||||
* app 只经本接口读该列表(不直接拼路径、不依赖 launcher 内存数组)。
|
||||
* - 渲染仍走 env MEOW_FONT_PNG/MEOW_FONT_IDX(launcher 注入的「当前生效字体」,
|
||||
* 选中字体那一套);本节的 get_available_count/enumerate 只负责「列有哪些库」,
|
||||
* select_by_id 负责「app 会话内固定用哪套」。两路明确。
|
||||
* - 无卡 / 无 index → 仅 1 项板载默认(family=default)。 */
|
||||
#ifndef MEOW_FONT_INFO_DEFINED
|
||||
#define MEOW_FONT_INFO_DEFINED
|
||||
typedef struct meow_font_info {
|
||||
char family[64]; /* family slug = MeowOS/fonts/<family>/ 一级目录名 */
|
||||
char name[64]; /* 显示名(index 无单列 → 回退 family) */
|
||||
char weight[32]; /* weight slug(regular|bold|…,缺省 regular) */
|
||||
char tier[8]; /* 档位 common|gbk|full(info 用) */
|
||||
int glyph_count; /* font.json glyph_count(info 用) */
|
||||
char label[128]; /* index 显示名(含字重 + 尺寸标记) */
|
||||
int size_list[16]; /* 原生字号表(index 无 → 空) */
|
||||
int size_count; /* 有效字号数 */
|
||||
int bit_depth; /* 4|8(index 无 → 0) */
|
||||
char path_png[320]; /* 卡根相对 MeowOS/fonts/<family>/<weight>/font.png */
|
||||
} meow_font_info;
|
||||
#endif
|
||||
|
||||
/* 可用字库数(含默认;无卡 → 1)。 */
|
||||
int meow_font_get_available_count(void);
|
||||
/* 按序号取:idx=0=默认行(family=default);越界返回 0。 */
|
||||
int meow_font_enumerate(int idx, meow_font_info *out);
|
||||
/* 选该套为 app 会话默认字体:id=0 跟随系统默认(font.file,经 env);id>0 固定用该套。
|
||||
* 该套字库缺失 → 降级默认(不丢请求)+ WARN(meow_log→app 日志)。成功 0,越界负值。 */
|
||||
int meow_font_select_by_id(int id);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §10 字体分层 / 大字库(M2.5x:基座声明 + font 域已实现按档装载) */
|
||||
/* ============================================================ */
|
||||
/* 字体「档位」概念:launcher 按 app.json 声明的字库档("fonts":<档>)向 app 注入
|
||||
* 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/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 定义并触发 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);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_FONT_H */
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* meow_ui_image.h — SDK 图片层(§4,B1)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(image 模块)。内部 stb_image(单头文件,静态编入 meow_ui.a)
|
||||
* 解 PNG/JPEG/BMP,统一转 RGBA。meow_img 为不透明句柄(前向声明见 meow_ui_types.h)。
|
||||
*/
|
||||
#ifndef MEOW_UI_IMAGE_H
|
||||
#define MEOW_UI_IMAGE_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §4 图片层 */
|
||||
/* ============================================================ */
|
||||
typedef struct meow_img meow_img; /* 句柄,由 meow_img_* 管理,不透明 */
|
||||
|
||||
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);
|
||||
|
||||
void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */
|
||||
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 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_IMAGE_H */
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* meow_ui_ime.h — SDK 输入法(§5.1,B1):内置触屏软键盘。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(ime 模块)。仅英文 QWERTY / 数字 / 中文拼音(ASCII/NUM/PINYIN)。
|
||||
* 输入事件循环/触摸命中优先级见 meow_ui_input.h 相关约定(IME 弹出时吞触摸)。
|
||||
*/
|
||||
#ifndef MEOW_UI_IME_H
|
||||
#define MEOW_UI_IME_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.1 输入法(IME)—— 内置触屏软键盘(仅英文 QWERTY / 数字,ASCII) */
|
||||
/* ============================================================ */
|
||||
/* 输入法模式(尾部追加枚举:ASCII/NUM 值不变,零 ABI 回归)。PINYIN=中文拼音(meow_ui_ime.c)。 */
|
||||
typedef enum { MEOW_IME_ASCII, MEOW_IME_NUM, MEOW_IME_PINYIN } meow_ime_mode;
|
||||
|
||||
typedef struct meow_ime_ctx {
|
||||
meow_ime_mode mode;
|
||||
char buf[256]; /* 输入缓冲(初始值可预填) */
|
||||
int maxlen; /* 最大字符数(含 NUL 预算) */
|
||||
void (*on_change)(const char *text, void *ctx); /* 每次字符变化回调一次 */
|
||||
void *ctx;
|
||||
} meow_ime_ctx;
|
||||
|
||||
/* 阻塞:弹出键盘直到关闭(ESC / 点背景取消);回车=只提交不关闭(内容经 on_change 实时同步)。
|
||||
* 关闭路径返回 0(取消);提交内容经 on_change 回调逐键同步,不依赖返回值回填 buf。 */
|
||||
int meow_ui_text_input(meow_ime_ctx *ctx);
|
||||
|
||||
/* 非阻塞:返回后 app 在 on_tick 里轮询 meow_ime_active()/meow_ime_text() */
|
||||
void meow_ui_text_input_async(meow_ime_ctx *ctx);
|
||||
int meow_ime_active(void);
|
||||
const char *meow_ime_text(void);
|
||||
|
||||
/* 拼音模式组合态:当前拼音组合串(去声调小写,如 "nihao")供 App 渲染字段预览;
|
||||
* 无组合(或非拼音模式)返回 ""。组合串是 IME 内部态,不写入字段(§4.2)。 */
|
||||
const char *meow_ime_composition(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_IME_H */
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* meow_ui_input.h — SDK 输入层 / 生命周期(§6,B1)+ 通用长按(§6.3,2026-08-31)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(input 模块)。物理键为系统语义(SDK 不下发 on_key 映射为确认/exit),
|
||||
* App 只响应触摸/滑动;事件循环阻塞到 meow_ui_request_exit。
|
||||
*/
|
||||
#ifndef MEOW_UI_INPUT_H
|
||||
#define MEOW_UI_INPUT_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §6 输入层 / 生命周期(B1) */
|
||||
/* ============================================================ */
|
||||
typedef struct meow_ui_events {
|
||||
void (*on_key)(int keycode, int is_down); /* M2.24:物理键为系统语义,SDK 不下发;回调保留(应用不映射键为确认/exit) */
|
||||
void (*on_tap)(int x, int y); /* 触摸抬起落点 */
|
||||
void (*on_swipe)(int dir, int dx, int dy); /* 左滑=返回(左缘起点+抬手触发) */
|
||||
void (*on_tick)(void); /* 空闲周期回调(动画/时钟) */
|
||||
void (*on_swipe_progress)(int x_start, int dx);/* 左缘返回手势过程实时回调;x_start=按下X, dx=当前X-按下X */
|
||||
/* M2.71-bug2/6:拖动过程回调(跟手)。on_touch_down=按下后首个有效坐标(起点);
|
||||
* on_touch_move=拖动中坐标变化(坐标变了才回调)。用于 slider 拖动 / 列表/页面滚动跟手。
|
||||
* 均在 IME 弹出或模态阻塞时被吞(键盘/模态优先喂)。可 NULL。 */
|
||||
void (*on_touch_down)(int x, int y);
|
||||
void (*on_touch_move)(int x, int y);
|
||||
} meow_ui_events;
|
||||
|
||||
void meow_ui_run(const meow_ui_events *ev); /* 阻塞:事件循环,直到 meow_ui_request_exit */
|
||||
void meow_ui_request_exit(void); /* 请求退出(相当于返回/退出键) */
|
||||
|
||||
/* 顶部左滑进度条(视觉反馈):画一条 5px 高、从 x=0 到 x 的横条(主题 accent 色)。
|
||||
* x<=0 时清掉此前画的条(填回背景色)。不发 flush,由调用方 meow_ui_flush_region 上屏。 */
|
||||
void meow_ui_swipe_hint(int x);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §6.3 通用长按(2026-08-31) */
|
||||
/* ============================================================ */
|
||||
/* 控件按下 ≥ 阈值(默认 500ms,可配)未抬手、且按下后位移 ≤ 容忍(超移动/抬手取消)
|
||||
* → 触发一次长按回调;坐标 = 触发时刻的按下点(App 据此 meow_widget_hit / meow_listview_hit
|
||||
* 判定命中哪个控件/行,与 on_tap 同一套命中流程)。
|
||||
*
|
||||
* **opt-in**:注册回调(meow_ui_set_long_press_cb)才启用长按判定;未注册时行为与之前完全一致
|
||||
* (按压不触发长按、抬手照常 TAP/SWIPE)——hello 等不使用长按的 App 零回归。
|
||||
*
|
||||
* 与 pressed 状态机共存:按压视觉态(MEOW_WS_PRESSED / 按住加深)不变;短按(未达阈值或
|
||||
* 抬手先于阈值)仍正常触发 on_tap 点击;长按已触发后其抬手**不再产生 TAP/SWIPE**(SDK 抑制
|
||||
* 点击,App 无需自行吞掉后续 tap)。
|
||||
*
|
||||
* 注册经独立 set 接口:**不做进 meow_ui_events**——避免给既有 App(hello/files)按位置初始化
|
||||
* meow_ui_events 的写法在 -Wextra 下引入「缺省成员」告警(_missing-field-initializers),
|
||||
* 保持 SDK 头向后兼容。 */
|
||||
typedef void (*meow_long_press_fn)(int x, int y);
|
||||
void meow_ui_set_long_press_cb(meow_long_press_fn cb); /* 注册/更换回调;NULL=取消 */
|
||||
void meow_ui_set_long_press_ms(int ms); /* 长按阈值(ms);<=0 恢复默认 500 */
|
||||
int meow_ui_long_press_ms(void); /* 当前阈值(默认 500) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_INPUT_H */
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* meow_ui_log.h — SDK 应用日志(§9,统一日志 API,M2.51 最小可用版)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(log 模块)。app 只需一行 meow_log();SDK 管「写哪/怎么写/过滤/降级/上限」。
|
||||
*/
|
||||
#ifndef MEOW_UI_LOG_H
|
||||
#define MEOW_UI_LOG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §9 应用日志(统一日志 API,M2.51 最小可用版) */
|
||||
/* ============================================================ */
|
||||
/* app 只需一行 meow_log();SDK 管「写哪/怎么写/过滤/降级/上限」:
|
||||
* - 路径:<MEOW_LOG_DIR>/<启动时间>_<pid>.log(MEOW_LOG_DIR 已含包名分层 logs/<包名>)
|
||||
* (目录不存在 mkdir -p,按 app 分目录),每次启动=新文件,同一次启动内 O_APPEND 追加;
|
||||
* - 级别过滤:lvl < meow_log_get_level() 丢弃;默认 MEOW_LOG_INFO;
|
||||
* - 格式:<monotonic_ms> [<app_id>] <LEVEL> message\n;
|
||||
* - 上限:单文件 ≥2MB 自动开新文件追加 _<n>;每 App 总量 ≥50MB 按期删最旧,防撑爆卡;
|
||||
* - 无卡/写失败降级 stderr,不阻塞 app 不崩;线程安全。
|
||||
* 注意:MEOW_LOG_APP 由 launcher 注入;未注入时默认 app_id="app"。 */
|
||||
typedef enum { MEOW_LOG_DEBUG=0, MEOW_LOG_INFO, MEOW_LOG_WARN, MEOW_LOG_ERROR } meow_log_level;
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...); /* app 一行调用,写到按 app 分离日志 */
|
||||
void meow_log_set_level(meow_log_level lvl); /* 可选:app 设本 app 级别;默认 MEOW_LOG_INFO */
|
||||
meow_log_level meow_log_get_level(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_LOG_H */
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* meow_ui_notify.h — SDK 应用通知(§9.1,meow_notify_*,M2.53 最小可用版)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(notify 模块)。app 只需声明类型 + meow_notify_send() 即可发通知,
|
||||
* SDK 管配额/活动表/降级。本阶段 SDK 内部维护活动通知表 + 用 meow_log() 打日志作为落点;
|
||||
* 真实投递到通知中心 UI / D-Bus 是后续任务(本结构已对齐 freedesktop 字段为此预留)。
|
||||
*/
|
||||
#ifndef MEOW_UI_NOTIFY_H
|
||||
#define MEOW_UI_NOTIFY_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §9.1 应用通知(meow_notify_*,M2.53 最小可用版) */
|
||||
/* ============================================================ */
|
||||
/* app 只需声明类型 + meow_notify_send() 即可发通知,SDK 管配额/活动表/降级:
|
||||
* - 类型注册:app 声明支持的类型(最多 MEOW_NOTIFY_MAX_TYPES 4 种);
|
||||
* meow_notify_send 也会自动注册未声明类型(受同样 4 种上限约束);
|
||||
* - 配额(NOTIFICATION_SYSTEM.md §7,2026-08-29 用户拍板):每 App 最多 4 种类型;
|
||||
* 每类型活动(未过期)通知上限 4,满则丢最旧(替换);媒体类型单例(每 app 仅
|
||||
* 保留最近 1 条,重发替换——可多媒体 App);
|
||||
* - 超时语义 [实现定案,沿用 NOTIFICATION_SYSTEM.md §2]:timeout_ms>0 = 过期毫秒数;
|
||||
* 0 = 不自动过期(直到 meow_notify_cancel);-1 = 常驻(ONGOING),同样不自动过期;
|
||||
* - 落点:本阶段 SDK 内部维护活动通知表 + 用 meow_log() 打日志作为落点;真实投递到
|
||||
* 通知中心 UI / D-Bus(org.freedesktop.Notifications)是后续任务——本结构已对齐
|
||||
* freedesktop 字段(summary/body/…)为此预留,但不写死后端;
|
||||
* - 线程安全;不依赖 /dev/fb0、不依赖板子,可宿主编译。 */
|
||||
#define MEOW_NOTIFY_MAX_TYPES 4 /* 每 App 最多 4 种通知类型 */
|
||||
#define MEOW_NOTIFY_MAX_PER_TYPE 4 /* 每类型活动(未过期)通知上限 */
|
||||
#define MEOW_NOTIFY_MAX_MEDIA 1 /* 媒体通知每 App 仅 1 条(单例) */
|
||||
#define MEOW_NOTIFY_TIMEOUT_ONGOING (-1) /* 常驻(ONGOING),不自动过期 */
|
||||
#define MEOW_NOTIFY_TIMEOUT_NEVER 0 /* 不自动过期,直到取消 */
|
||||
|
||||
/* 通知分类(freedesktop.org Desktop Notifications 的 category 最小子集 [实现定案]) */
|
||||
typedef enum {
|
||||
MEOW_NOTIFY_CAT_GENERAL = 0, /* 通用(默认) */
|
||||
MEOW_NOTIFY_CAT_MESSAGE = 1, /* 即时消息/聊天 */
|
||||
MEOW_NOTIFY_CAT_EMAIL = 2, /* 邮件 */
|
||||
MEOW_NOTIFY_CAT_EVENT = 3, /* 事件/日历提醒 */
|
||||
MEOW_NOTIFY_CAT_PROGRESS = 4, /* 进度/下载 */
|
||||
MEOW_NOTIFY_CAT_MEDIA = 5, /* 媒体播放(MPRIS;每 app 单例) */
|
||||
MEOW_NOTIFY_CAT_COUNT = 6, /* 边界哨兵(非合法类型) */
|
||||
} meow_notify_category;
|
||||
|
||||
/* 紧急度(UI 视觉分级 + 提示音/震动强弱) */
|
||||
typedef enum {
|
||||
MEOW_NOTIFY_URG_LOW = 0, /* 低:仅进通知中心,不打扰 */
|
||||
MEOW_NOTIFY_URG_NORMAL = 1, /* 普通 */
|
||||
MEOW_NOTIFY_URG_HIGH = 2, /* 高:顶部横幅 + 提示音 */
|
||||
MEOW_NOTIFY_URG_CRITICAL = 3, /* 紧急:需立刻注意 */
|
||||
MEOW_NOTIFY_URG_COUNT = 4, /* 边界哨兵(非合法值) */
|
||||
} meow_notify_urgency;
|
||||
|
||||
/* 发送负载(对齐 freedesktop Notify() 字段;SDK 内部复制副本,不持 app 外部指针,
|
||||
* app 只需保证 icon/title/body/target_activity 指针在调用返回前有效)。 */
|
||||
typedef struct meow_notify {
|
||||
meow_notify_category cat; /* 类型(未声明则按 4 种上限自动注册) */
|
||||
meow_notify_urgency urgency; /* 紧急度(缺省 MEOW_NOTIFY_URG_NORMAL;越界钳位) */
|
||||
const char *title; /* summary:标题,单行(必填,NULL 则发送失败) */
|
||||
const char *body; /* body:正文,可多行(可 NULL) */
|
||||
const char *icon; /* app_icon:图标 icon name 或 path;NULL/空 = 用该 app 默认图标(launcher 按 app_id 回退) */
|
||||
int timeout_ms; /* 见上超时语义;推荐 MEOW_NOTIFY_TIMEOUT_* */
|
||||
const char *target_activity; /* 通知中心点击跳转的 Activity 名(可 NULL;M3 导航预留) */
|
||||
int progress; /* 进度 0-100;-1=不显示进度条(缺省 -1) */
|
||||
} meow_notify;
|
||||
|
||||
/* 类型注册:app 声明支持的类型。0=成功;-1=已声明/已达 4 种上限/非法。 */
|
||||
int meow_notify_register_type(meow_notify_category cat);
|
||||
|
||||
/* 发送一条通知。返回通知 id(用于 meow_notify_cancel);失败返回 0。
|
||||
* 同类型满 4 条时丢弃最旧(替换);媒体类型单例(重发替换同 app 上一条媒体通知)。 */
|
||||
uint32_t meow_notify_send(const meow_notify *n);
|
||||
|
||||
/* 取消一条通知。1=成功;0=未找到(id 无效/已过期/已取消)。 */
|
||||
int meow_notify_cancel(uint32_t id);
|
||||
|
||||
/* 实时更新一条活动通知的进度(0-100,-1=清除进度条)。local_id=meow_notify_send 的返回值。
|
||||
* 语义(用户定案):仅改 SDK 内部活动槽 + 经 IPC 通知 launcher 改**内存活跃槽**;不落盘——
|
||||
* TF 快照只在「创建时 / 结束(100% 或 dismiss)」触发。返回 1=找到并更新;0=未找到/非法。 */
|
||||
int meow_notify_update_progress(uint32_t local_id, int progress);
|
||||
|
||||
/* 查询当前活动(未过期、未取消)通知数/是否还有。 */
|
||||
int meow_notify_active_count(void);
|
||||
int meow_notify_count_in_type(meow_notify_category cat);
|
||||
int meow_notify_has_active(void); /* 1=尚有活动通知,0=无 */
|
||||
|
||||
/* 紧急度/分类 → 字符串("low"/"normal"/"high"/"critical"、"general"/"message"/…)。 */
|
||||
const char *meow_notify_urgency_str(meow_notify_urgency u);
|
||||
const char *meow_notify_category_str(meow_notify_category cat);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_NOTIFY_H */
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* meow_ui_statusbar.h — SDK 全局状态栏 / 全屏(§3.1,A-0,系统级组件)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(statusbar 模块)。状态栏是 SDK 内置全局组件:默认显示(时钟/电量/
|
||||
* WiFi/蓝牙/音量),数据由 SDK 内置 meow_ui_sys 读取器 1s 轻刷(与 launcher 同源单点一致)。
|
||||
* z 顺序最顶:meow_ui_flush() 上屏前自动把状态栏画进 back_buf 顶部行,覆盖非全屏内容;
|
||||
* 非全屏 APP 需用 meow_ui_statusbar_height() 把内容下移(inset),否则被状态栏盖住。
|
||||
* 注:状态栏的**渲染**仅系统进程(launcher)执行;App 进程只按本头声明控制显隐/全屏/inset。
|
||||
*/
|
||||
#ifndef MEOW_UI_STATUSBAR_H
|
||||
#define MEOW_UI_STATUSBAR_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §3.1 全局状态栏 / 全屏(A-0,SDK 系统级组件) */
|
||||
/* ============================================================ */
|
||||
/* 状态栏是 SDK 内置全局组件:所有界面默认显示(时钟/电量/WiFi/蓝牙/音量,slim ~40px),
|
||||
* 数据由 SDK 内置 meow_ui_sys 读取器 1s 轻刷(与 launcher 同源单点一致)。
|
||||
* z 顺序最顶:meow_ui_flush() 上屏前自动把状态栏画进 back_buf 顶部行,覆盖非全屏内容;
|
||||
* 非全屏 APP 需用 meow_ui_statusbar_height() 把内容下移(inset),否则被状态栏盖住。 */
|
||||
|
||||
/* 状态栏显隐(默认显示;APP 声明全屏则隐藏——沉浸)。 */
|
||||
void meow_ui_set_statusbar(int visible); /* 1=显示(默认) 0=隐藏(全屏) */
|
||||
int meow_ui_statusbar_visible(void);
|
||||
|
||||
/* 全屏声明:等价 app.json fullscreen:1(外观上隐藏状态栏 + 左缘返回手势/进度条——沉浸)。 */
|
||||
void meow_ui_set_fullscreen(int on);
|
||||
|
||||
/* 状态栏高度(px,供 APP 布局上移内容,避免被盖——status bar inset);
|
||||
* 全屏/隐藏时返回 0。 */
|
||||
int meow_ui_statusbar_height(void);
|
||||
|
||||
/* ---- 状态栏「上收出屏只留提示线」(collapsed,草案/待定案——本批仅加声明)----
|
||||
* 供全屏调用:把状态栏内容上移出屏顶、只留一条「提示线」高度(可下拉还原)。
|
||||
* 与 meow_ui_set_fullscreen(1) 的关系:fullscreen 隐藏整条状态栏(沉浸,inset=0);
|
||||
* collapsed 仍视状态栏为「激活」,左缘返回手势(SWIPE_LEFT)与顶部伸缩提示线
|
||||
* (meow_ui_swipe_hint)依旧生效,只是其内容上收、仅留提示线——用于「不完全沉浸」,
|
||||
* 保留一处可下拉拉出状态栏的入口。顶部下滑热区 meow_ui_statusbar_hotzone() 在全屏时缩小。
|
||||
* 实现位置:meow_ui_gfx.c(基座让 on 生效:true→只画提示线/内容上移);逐帧过渡动画
|
||||
* (上收/恢复插值)为待定案,后续代理可经 meow_ui_statusbar_animate(ms) 补齐。 */
|
||||
void meow_ui_statusbar_set_collapsed(int on); /* on=1 上收(内容上移出屏、只留提示线);on=0 恢复 */
|
||||
int meow_ui_statusbar_collapsed(void); /* 当前是否处于「上收」目标(1=是) */
|
||||
void meow_ui_statusbar_animate(int ms); /* TBD:上收/恢复过渡时长(ms);占位,后续代理实现插值 */
|
||||
/* 当前上收进度(0..MUI_SBAR_H:0=完全展开,28=内容全部上移出屏、仅留提示线)。
|
||||
* 供系统进程(launcher)在 App 前台叠加顶条时读取,按「当前可见条高 = 28-offset」
|
||||
* 标脏/切齐,实现收合时只 blit 可见条高、其余行保留 App 内容(平滑上收、无残留)。 */
|
||||
int meow_ui_statusbar_collapse_offset(void);
|
||||
/* 上收/恢复过渡是否仍在进行(current offset != target,逐帧推进中)。
|
||||
* 供 launcher 在过渡期高频刷新顶条(launch_app 循环据此缩短 sleep),驱动平滑动画;稳定后回 1s 慢刷。 */
|
||||
int meow_ui_statusbar_animating(void);
|
||||
|
||||
/* 顶部下滑热区(下拉展开状态栏/通知)高度(px)。
|
||||
* 状态栏「激活」(含**收合**)时恒=整条状态栏高 MUI_SBAR_H(28)——收合只上收渲染、**不禁用顶部热区**
|
||||
* (渲染与热区事件分离,全屏下顶部下滑仍开通知中心);仅「全屏隐藏」(immersive inset0)时缩小到
|
||||
* MEOW_SBAR_HOTZONE_FS(保留一处下拉入口)。返回「可用于起始下拉」的热区高度。 */
|
||||
int meow_ui_statusbar_hotzone(void);
|
||||
#define MEOW_SBAR_HOTZONE_FS 4 /* 全屏时缩小后的顶部热区高度(px,草案值) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_STATUSBAR_H */
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* meow_ui_theme.h — SDK 主题/配置(§8):主题模型 + MD3 语义色板 + 顶级主题风格族。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(theme 模块)。权威结构体名 = meow_theme(App 侧),
|
||||
* 与 launcher 内部的 meow_ui_theme_t(font_theme.c,仅供状态栏/设置页)无关。
|
||||
*/
|
||||
#ifndef MEOW_UI_THEME_H
|
||||
#define MEOW_UI_THEME_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §8 主题/配置 */
|
||||
/* ============================================================ */
|
||||
typedef enum { MEOW_THEME_LIGHT, MEOW_THEME_DARK, MEOW_THEME_AUTO } meow_theme_mode;
|
||||
|
||||
/* 主题(§5.8.4 + §8.1.3)。既有字段 bg/fg/accent/dim 保留(兼容映射,见下表),
|
||||
* 其后追加 MD3 语义色板 + 风格参数段(扩展兼容——既有代码读旧字段不受影响)。
|
||||
* 映射:bg↔surface、fg↔onSurface、accent↔primary、dim↔onSurfaceVariant。
|
||||
* 「当前 style×mode」完整主题 = meow_theme_get()(MD3 默认;MIUIx 用 MD3 基础/占位)。 */
|
||||
typedef struct meow_theme {
|
||||
/* —— 既有字段(兼容保留;映射见上)—— */
|
||||
uint32_t bg; /* 背景色 = surface */
|
||||
uint32_t fg; /* 前景/正文 = onSurface */
|
||||
uint32_t accent; /* 强调色(按钮/选中)= primary */
|
||||
uint32_t dim; /* 次要文字 = onSurfaceVariant */
|
||||
int font_title; /* 28 */
|
||||
int font_body; /* 20 */
|
||||
|
||||
/* —— MD3 语义色板(§5.8.4;日/夜各一组,AUTO 按 meow_theme_mode 切换)—— */
|
||||
uint32_t primary, onPrimary, primaryContainer, onPrimaryContainer;
|
||||
uint32_t secondary, onSecondary, secondaryContainer, onSecondaryContainer;
|
||||
uint32_t tertiary, onTertiary, tertiaryContainer, onTertiaryContainer;
|
||||
uint32_t surface, onSurface, surfaceVariant, onSurfaceVariant;
|
||||
uint32_t surfaceContainerLowest, surfaceContainerLow, surfaceContainer,
|
||||
surfaceContainerHigh, surfaceContainerHighest;
|
||||
uint32_t surfaceDim, surfaceBright, surfaceTint;
|
||||
uint32_t outline, outlineVariant, scrim;
|
||||
uint32_t error, onError, errorContainer, onErrorContainer;
|
||||
uint32_t inverseSurface, inverseOnSurface, inversePrimary;
|
||||
uint32_t shadow; /* 仅状态层/描边近似用;无 GPU 不做投影(§5.8.7) */
|
||||
|
||||
/* —— 风格参数段(§8.1.3;MD3 默认 / MIUIx 加大圆角+描边+间距)—— */
|
||||
int radius; /* 控件默认圆角(按钮等,px) */
|
||||
int radius_card; /* 卡片/容器默认圆角(px) */
|
||||
int spacing; /* 布局/间距步进(px) */
|
||||
int stroke_w; /* 默认细描边宽(0=无边框,MD3;MIUIx=1) */
|
||||
} meow_theme;
|
||||
|
||||
const meow_theme *meow_theme_get(void);
|
||||
void meow_theme_set_mode(meow_theme_mode m);
|
||||
meow_theme_mode meow_theme_get_mode(void);
|
||||
|
||||
typedef void (*meow_theme_listener)(meow_theme_mode m);
|
||||
void meow_theme_add_listener(meow_theme_listener cb);
|
||||
void meow_theme_remove_listener(meow_theme_listener cb);
|
||||
|
||||
/* [D4] 脱机访问两套内置色板(无 settings 文件也可靠):返回当前解析生效的色板副本。 */
|
||||
meow_theme meow_theme_defaults(meow_theme_mode m);
|
||||
|
||||
/* ---- §8.1 顶级主题风格族(minimal,2026-08-30)----
|
||||
* 主题 = 顶级主题风格族(ui.style:MD3/MIUIx)× 明暗(display.theme)。本批只落「状态栏图标集
|
||||
* 随 ui.style 换集」的最小闭环:meow_ui_set_style 记录 style(同值忽略)+ 触发 theme_reload/
|
||||
* 广播(让状态栏/控件下一帧用新 style 重绘)。style 还决定状态栏右侧图标集(§3.1 同一个状态栏)。
|
||||
* 完整 style×mode 配色/控件形态矩阵(§8.1.3)为后续里程碑。 */
|
||||
#define MEOW_STYLE_MD3 0 /* Material You(默认) */
|
||||
#define MEOW_STYLE_MIUIX 1 /* MIUI 风格族(基础占位) */
|
||||
|
||||
void meow_ui_set_style(int style); /* 设顶级主题风格族;越界 clamp 到合法值;同值忽略 */
|
||||
int meow_ui_style(void); /* 当前 style(默认 MEOW_STYLE_MD3) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_THEME_H */
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* meow_ui_types.h — SDK 公共核心类型与常量(无依赖基础层)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分:本头只含「无模块依赖」的基础件——
|
||||
* - 物理键 keycode(内核 input.h 常用值;KEEP 与 launcher/settings 一致)
|
||||
* - 滑动方向(on_swipe 的 dir 参数)
|
||||
* - 各模块共用的不透明句柄前向声明(meow_widget / meow_img)
|
||||
* 其它所有子头/App 经 #include 本头获得这些基础件;伞头 meow_ui.h 聚合它。
|
||||
*/
|
||||
#ifndef MEOW_UI_TYPES_H
|
||||
#define MEOW_UI_TYPES_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* 物理键 keycode(内核 input.h 常用值;KEEP 与 launcher/settings 一致) */
|
||||
/* ============================================================ */
|
||||
#define MEOW_KEY_POWER 116
|
||||
#define MEOW_KEY_VOLUMEDOWN 114
|
||||
#define MEOW_KEY_VOLUMEUP 115
|
||||
#define MEOW_KEY_PLAYPAUSE 164
|
||||
#define MEOW_KEY_PREVIOUS 165 /* KEY_PREVIOUSSONG=165(上一曲;19 是另一码,勿混用) */
|
||||
#define MEOW_KEY_NEXT 163 /* KEY_NEXTSONG */
|
||||
|
||||
/* 滑动方向(on_swipe 的 dir 参数) */
|
||||
#define MEOW_SWIPE_LEFT 0
|
||||
#define MEOW_SWIPE_RIGHT 1
|
||||
#define MEOW_SWIPE_UP 2
|
||||
#define MEOW_SWIPE_DOWN 3
|
||||
|
||||
/* ---- 不透明句柄(各 App/子头共用;具体结构定义在对应实现 TU)---- */
|
||||
typedef struct meow_widget meow_widget; /* 控件(meow_ui_widget.c) */
|
||||
typedef struct meow_img meow_img; /* 图片句柄(meow_ui_img.c) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_TYPES_H */
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* meow_ui_widgets.h — SDK 控件层(§5 控件)+ 控件层升级(§5.8 基座)+ Material 3 组件子集(§5.8.6)
|
||||
* + 结构化列表 W_LISTVIEW(§5.5)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(widgets 模块)。依赖 meow_ui_theme.h(meow_theme/MD3 色板/风格参数)
|
||||
* 与 meow_ui_ime.h(meow_ime_mode,TextField 接 IME);meow_img 为不透明句柄(meow_ui_types.h)。
|
||||
* 向后兼容:既有 create 签名一字不改,只在上面扩展(Options/状态机走新增挂载器)。
|
||||
*/
|
||||
#ifndef MEOW_UI_WIDGETS_H
|
||||
#define MEOW_UI_WIDGETS_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
#include "meow_ui_theme.h"
|
||||
#include "meow_ui_ime.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5 控件层 */
|
||||
/* ============================================================ */
|
||||
typedef struct meow_widget meow_widget;
|
||||
|
||||
meow_widget *meow_btn_create(int x, int y, int w, int h, const char *label);
|
||||
meow_widget *meow_slider_create(int x, int y, int w, int min, int max, int val);
|
||||
meow_widget *meow_switch_create(int x, int y, int on);
|
||||
/* 列表数据源回调:idx 有效时填 label 并返回 1;越界返回 0([D2] 定案签名) */
|
||||
typedef int (*meow_list_source)(int idx, char *label, int len, void *ctx);
|
||||
meow_widget *meow_list_create(int x, int y, int w, int h, int count,
|
||||
meow_list_source src, void *ctx);
|
||||
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); /* 点是否落在控件内 */
|
||||
|
||||
void meow_slider_set(meow_widget *s, int val);
|
||||
int meow_slider_get(meow_widget *s);
|
||||
void meow_switch_set(meow_widget *w, int on);
|
||||
int meow_switch_get(meow_widget *s);
|
||||
int meow_btn_pressed(meow_widget *w); /* 应用在 on_tap 命中后标记按下 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.8 控件层升级(第一批=通用基座):Options / 状态机 / 自定义画布 / 布局 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* ---- §5.8.2 Modifier 式通用 Options(轻量结构体,非链式 DSL)----
|
||||
* 经 meow_widget_set_opts 一次性挂载,作用于任意控件(标准/自定义)。全部控件共用。
|
||||
* 哨兵语义:bg/fg==0=用控件/主题默认;stroke==0=无描边;stroke_w=描边宽;
|
||||
* radius==-1=控件默认(如按钮 10),>=0=指定圆角(0=直角);pad/margin=0=无;
|
||||
* disabled/selected=1=置位(立即反映到状态机位)。 */
|
||||
typedef struct meow_widget_opts {
|
||||
uint32_t bg; /* 背景色(0=控件/主题默认) */
|
||||
uint32_t fg; /* 前景/文字(0=主题 onSurface) */
|
||||
uint32_t stroke; /* 描边色(0=无描边) */
|
||||
int stroke_w; /* 描边宽(px) */
|
||||
int radius; /* 圆角(px;-1=控件默认,如按钮 10) */
|
||||
int pad_x; /* 内边距(水平,px) */
|
||||
int pad_y; /* 内边距(垂直,px) */
|
||||
int margin_x; /* 外边距(水平,px;布局用) */
|
||||
int margin_y; /* 外边距(垂直,px;布局用) */
|
||||
int disabled; /* 1=禁用(视觉变暗 + 不响应命中,见状态机 §5.8.3) */
|
||||
int selected; /* 1=选中(列表高亮/勾选,见状态机 §5.8.3) */
|
||||
} meow_widget_opts;
|
||||
|
||||
/* 挂载:作用于任意控件(标准/自定义)。w 内部浅拷这份 opts。
|
||||
* 返回 0=成功;NULL/无效=负值(不销毁 w)。 */
|
||||
int meow_widget_set_opts(meow_widget *w, const meow_widget_opts *opts);
|
||||
/* 取回当前 opts(浅拷到 out;0=成功)。自定义画布用它读通用修饰。 */
|
||||
int meow_widget_opts_get(const meow_widget *w, meow_widget_opts *out);
|
||||
|
||||
/* ---- §5.8.3 统一状态机(enabled / selected / focused / pressed)----
|
||||
* 标准/自定义控件共用;visual 层由 SDK/App 按状态位叠。enabled=0 阻断 hit/pressed。 */
|
||||
#define MEOW_WS_ENABLED (1u<<0)
|
||||
#define MEOW_WS_SELECTED (1u<<1)
|
||||
#define MEOW_WS_FOCUSED (1u<<2)
|
||||
#define MEOW_WS_PRESSED (1u<<3)
|
||||
|
||||
/* 设/查状态(置位/清位,其它位不动)。on=1 置位 / 0 清位。 */
|
||||
void meow_widget_set_state(meow_widget *w, unsigned bits, int on);
|
||||
unsigned meow_widget_state(const meow_widget *w);
|
||||
|
||||
/* 便捷:禁用/启用、选中/取消(单态等效于 set_state)。 */
|
||||
int meow_widget_enabled(const meow_widget *w);
|
||||
void meow_widget_set_enabled(meow_widget *w, int on);
|
||||
int meow_widget_selected(const meow_widget *w);
|
||||
void meow_widget_set_selected(meow_widget *w, int on);
|
||||
|
||||
/* 一键叠状态层(自定义画布可选):把 pressed/focused/disabled/selected 的视觉层
|
||||
* 叠到 w 的矩形上(半透明状态层 + 焦点描边)。标准控件由 SDK 自动叠,无需调用;
|
||||
* 自定义画布在 render_cb 里画完内容后可调它(App 也可据 meow_widget_state 自行叠)。 */
|
||||
void meow_widget_draw_state_layer(meow_widget *w, const meow_theme *th);
|
||||
|
||||
/* ---- §5.8.1 自定义画布(App 提供 render/hit 回调;同一个 meow_widget)----
|
||||
* render:SDK 在 meow_widget_draw 时调用,th=当前生效主题(MD3 语义色板);
|
||||
* w 的 x/y/w/h 已就位;App 用 §3/§4 绘制 API 自绘(往 back_buf 画)。
|
||||
* hit :命中判定,App 自定义(如圆形/区域外镂空);返回 1=命中。 */
|
||||
typedef void (*meow_widget_render_fn)(meow_widget *w, const meow_theme *th, void *ctx);
|
||||
typedef int (*meow_widget_hit_fn)(meow_widget *w, int x, int y, void *ctx);
|
||||
|
||||
/* 创建:自定义画布(x,y,w,h = 绝对坐标,同既有控件)。
|
||||
* 生命周期/Activity 归属:创建时绑定当前 active Activity(同 meow_btn_create,§6.1 ①);
|
||||
* 命中走 hit_cb(先过 owner==active + enabled 双闸门)。 */
|
||||
meow_widget *meow_widget_custom_create(int x, int y, int w, int h,
|
||||
meow_widget_render_fn render_cb,
|
||||
meow_widget_hit_fn hit_cb,
|
||||
void *ctx);
|
||||
|
||||
/* ---- §5.8.5 可组合布局(Row / Column / Box + Spacer)----
|
||||
* 布局容器 = meow_widget(W_LAYOUT);arrange 把子控件算成绝对坐标(写进各 child 的
|
||||
* x/y/w/h)→ 与现有绝对定位完全兼容(draw/hit/flush_region/owner 闸门照常)。
|
||||
* 本批先支持固定尺寸子项(w/h 均已知);弹性(w/h==0)为后续增强。 */
|
||||
enum { MEOW_LAYOUT_ROW = 0, MEOW_LAYOUT_COLUMN = 1, MEOW_LAYOUT_BOX = 2 };
|
||||
|
||||
meow_widget *meow_row_create(int x, int y, int w, int h, int spacing);
|
||||
meow_widget *meow_column_create(int x, int y, int w, int h, int spacing);
|
||||
meow_widget *meow_box_create(int x, int y, int w, int h);
|
||||
|
||||
/* 添加子控件(子可为普通控件/自定义/另一布局/Spacer)。返回 0=成功;无效=负值。 */
|
||||
int meow_layout_add(meow_widget *layout, meow_widget *child);
|
||||
/* 计算子控件绝对坐标(写入各 child 的 x,y,w/h;嵌套布局递归);此后照常 draw/hit。 */
|
||||
void meow_layout_arrange(meow_widget *layout);
|
||||
|
||||
/* 固定尺寸空白(也作撑余量;不可见,hit 恒 0)。 */
|
||||
meow_widget *meow_layout_spacer(int w, int h);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.8.6 Material 3 组件子集(第二批,基于第一批基座:Options + 状态机 + MD3 色板 + 布局)
|
||||
* 每个组件 = 新增 meow_widget 类型或组合既有模板的便捷函数;复用主题语义色(th->primary /
|
||||
* surfaceContainer / outline / ...)+ 风格参数(radius/radius_card/spacing/stroke_w)+ 通用
|
||||
* Options(meow_widget_set_opts 对它们同样生效)+ 状态机(enabled/selected/pressed/focused)。
|
||||
* 向后兼容:既有 create 签名一字不改;本批只在其上扩展。 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* ---- Button 五型(meow_btn 扩展;既有 meow_btn_create = FILLED)---- */
|
||||
enum {
|
||||
MEOW_BTN_FILLED = 0, /* primary 底 / onPrimary 文 */
|
||||
MEOW_BTN_TONAL = 1, /* secondaryContainer 底 / onSecondaryContainer 文 */
|
||||
MEOW_BTN_OUTLINED = 2, /* 透明底 / primary 文 / primary 描边 */
|
||||
MEOW_BTN_TEXT = 3, /* 透明底 / primary 文 / 无描边 */
|
||||
MEOW_BTN_ELEVATED = 4, /* surfaceContainerLow 底 / primary 文 */
|
||||
};
|
||||
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=成功 */
|
||||
|
||||
/* ---- 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,
|
||||
char *buf, int maxlen, meow_ime_mode mode);
|
||||
void meow_textfield_set_placeholder(meow_widget *w, const char *placeholder);
|
||||
/* 聚焦并打开**阻塞** IME(同 meow_ui_text_input 内层轮询);回车不关闭键盘,ESC/点背景关闭。
|
||||
* 内容经 on_change 实时同步到 w->buf(返回值关闭路径为 0,提交不依赖返回值回填)。 */
|
||||
int meow_textfield_activate(meow_widget *w);
|
||||
const char *meow_textfield_text(meow_widget *w);
|
||||
|
||||
/* ---- Card(圆角容器卡片;surfaceContainerLow 底 + 可选描边/状态层)---- */
|
||||
meow_widget *meow_card_create(int x, int y, int w, int h);
|
||||
/* 内容子控件:绘制时递归绘到卡片内(卡片 hit 仍为矩形;子控件独立命中) */
|
||||
void meow_card_set_content(meow_widget *card, meow_widget *child);
|
||||
|
||||
/* ---- Divider(分隔线;vertical=1 纵向)---- */
|
||||
meow_widget *meow_divider_create(int x, int y, int len, int vertical);
|
||||
|
||||
/* ---- Badge(角标;count=0 红点 / >0 数字 / <0 隐藏;cx,cy=锚点=角标中心)---- */
|
||||
meow_widget *meow_badge_create(int cx, int cy);
|
||||
void meow_badge_set(meow_widget *w, int count);
|
||||
|
||||
/* ---- Checkbox / Radio(勾选态走 meow_widget_set_selected/meow_widget_selected)---- */
|
||||
meow_widget *meow_checkbox_create(int x, int y);
|
||||
meow_widget *meow_radio_create(int x, int y);
|
||||
|
||||
/* ---- ProgressBar(线性/圆形;value=0-100 确定性,-1=不定)---- */
|
||||
meow_widget *meow_progress_create(int x, int y, int w, int h, int value); /* 线性 */
|
||||
meow_widget *meow_progress_circular_create(int cx, int cy, int r, int value); /* 圆形 */
|
||||
void meow_progress_set(meow_widget *w, int value); /* -1=不定 */
|
||||
|
||||
/* ---- FloatingActionButton(悬浮按钮;圆角胶囊,无投影仅状态层近似)---- */
|
||||
meow_widget *meow_fab_create(int x, int y, int size, const char *label);
|
||||
|
||||
/* ---- TopAppBar(顶部标题栏:标题 + 返回 + 动作)---- */
|
||||
meow_widget *meow_topappbar_create(int x, int y, int w, int h, const char *title);
|
||||
void meow_topappbar_set_title(meow_widget *w, const char *title);
|
||||
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=无 */
|
||||
|
||||
/* ---- NavigationBar(底部导航栏)---- */
|
||||
meow_widget *meow_navigationbar_create(int x, int y, int w, int h);
|
||||
int meow_navigationbar_add_item(meow_widget *w, const char *label);
|
||||
void meow_navigationbar_set_selected(meow_widget *w, int idx);
|
||||
int meow_navigationbar_selected(meow_widget *w);
|
||||
int meow_navigationbar_hit(meow_widget *w, int x, int y); /* 返回项下标/-1 */
|
||||
|
||||
/* ---- TabBar(顶部分页页签;swipe 联动 §5.2 由 App 在 on_swipe 驱动)---- */
|
||||
meow_widget *meow_tabbar_create(int x, int y, int w, int h);
|
||||
int meow_tabbar_add_tab(meow_widget *w, const char *label);
|
||||
void meow_tabbar_set_selected(meow_widget *w, int idx);
|
||||
int meow_tabbar_hit(meow_widget *w, int x, int y); /* 返回页签下标/-1 */
|
||||
|
||||
/* ---- 列表容器(LazyColumn/LazyRow:数据源回调 + 可见区虚拟化;复用 meow_list_source)
|
||||
* meow_lazycolumn_create=纵向(item_h=行高);meow_lazyrow_create=横向(item_w=列宽)。
|
||||
* 滚动由 App 在 on_swipe 里用 meow_list_scroll_by/set_scroll 驱动。 */
|
||||
meow_widget *meow_lazycolumn_create(int x, int y, int w, int h, int item_h, int count,
|
||||
meow_list_source src, void *ctx);
|
||||
meow_widget *meow_lazyrow_create(int x, int y, int w, int h, int item_w, int count,
|
||||
meow_list_source src, void *ctx);
|
||||
void meow_list_set_scroll(meow_widget *w, int offset); /* clamp 到 [0, max_scroll] */
|
||||
int meow_list_scroll(meow_widget *w);
|
||||
int meow_list_max_scroll(meow_widget *w); /* max(0, count*item - viewport) */
|
||||
void meow_list_scroll_by(meow_widget *w, int dy); /* 相对滚动(App on_swipe 用) */
|
||||
int meow_list_set_item_h(meow_widget *w, int item); /* 改行高/列宽 */
|
||||
int meow_list_hit_index(meow_widget *w, int x, int y); /* 命中项下标(含滚动换算);-1=未命中项/越界 */
|
||||
/* M2.71-bug8:列表行选中 + 行点击回调。选中行已有高亮(现有绘制读 w->sel)。
|
||||
* meow_list_tap_at:命中一行则置选中并回调 tap_cb(若设),返回该行 idx;未命中返回 -1。 */
|
||||
void meow_list_set_selected(meow_widget *w, int idx); /* 选中行(越界 clamp);标脏 */
|
||||
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 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* §5.5 设计模板 A② 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作 + 选中
|
||||
* ------------------------------------------------------------
|
||||
* 在既有「label-only」列表(meow_list_create/lazycolumn,行为不变)之上做**结构化扩展**:
|
||||
* 行数据源回调逐行给出 meow_list_item(图标 / 标题 / 副标题 / 右侧操作 / 可选中),容器负责
|
||||
* 绘制/滚动/选中/命中;支持单选/多选/无选择。向后兼容:既有 meow_list_* 全部保留不动。
|
||||
* 颜色全部取 meow_theme_get() 语义色(日/夜/style 自动),不硬编码。
|
||||
* 命中:meow_listview_hit 返回行号(含滚动换算 + 禁用行不响应);行右 1/3 为右侧操作区
|
||||
* (meow_listview_hit_op 另判)。抬手用最新坐标(goodix 免疫,参照既有 W_LIST 习惯)。 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 选中模式:无(纯入口列表)/ 单选 / 多选。 */
|
||||
typedef enum { MEOW_LIST_SEL_NONE = 0, MEOW_LIST_SEL_SINGLE, MEOW_LIST_SEL_MULTI } meow_list_sel;
|
||||
|
||||
/* 行图标:meow_img*(MEOW_LIST_ICON_IMG)或内置符号(FOLDER/FILE,无图 App 用)。 */
|
||||
typedef enum {
|
||||
MEOW_LIST_ICON_NONE = 0, /* 无图标 */
|
||||
MEOW_LIST_ICON_IMG, /* 用 item->icon(meow_img*) */
|
||||
MEOW_LIST_ICON_FOLDER, /* 内置目录符号(填充矩形+页签) */
|
||||
MEOW_LIST_ICON_FILE, /* 内置文件符号(空心矩形+折角) */
|
||||
} meow_list_icon_kind;
|
||||
|
||||
/* 行右侧操作类型:无 / 箭头 / 开关 / 复选 / 详情钮(⋯)。 */
|
||||
typedef enum {
|
||||
MEOW_LIST_RIGHT_NONE = 0, /* 无右侧操作 */
|
||||
MEOW_LIST_RIGHT_ARROW, /* 箭头 › */
|
||||
MEOW_LIST_RIGHT_SWITCH, /* 开关(on/off = item->on) */
|
||||
MEOW_LIST_RIGHT_CHECK, /* 复选标记 */
|
||||
MEOW_LIST_RIGHT_MORE, /* ⋯ 详情钮 */
|
||||
} meow_list_right;
|
||||
|
||||
/* 单行数据(回调填充到栈上,容器不长期持有)。title 必填;subtitle 可空。 */
|
||||
typedef struct meow_list_item {
|
||||
const meow_img *icon; /* 可选(icon_kind==MEOW_LIST_ICON_IMG 时用) */
|
||||
int icon_kind; /* MEOW_LIST_ICON_*(NONE=无图标) */
|
||||
const char *title; /* 主文本(必填,NULL 视为空行) */
|
||||
const char *subtitle; /* 副文本,可空 */
|
||||
int right; /* MEOW_LIST_RIGHT_* 右侧操作类型 */
|
||||
int on; /* right==SWITCH 时开关态(0/1) */
|
||||
int enabled; /* 1=可选中/可交互;0=变淡不响应命中 */
|
||||
} meow_list_item;
|
||||
|
||||
/* 行数据源回调:idx 有效时填充 out 并返回 1;越界返回 0。每行轻量填充(不做 IO,§5.5)。 */
|
||||
typedef int (*meow_list2_source)(int idx, meow_list_item *out, void *ctx);
|
||||
|
||||
/* 创建:视口(x,y,w,h) + 行数 count + 行数据源 + 选中模式。行高默认 56(可用 set_item_h 改)。 */
|
||||
meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
|
||||
meow_list2_source src, void *ctx, meow_list_sel mode);
|
||||
/* 行高(固定;越界/<=0 忽略)。 */
|
||||
int meow_listview_set_item_h(meow_widget *lv, int item);
|
||||
/* 行数(搜索过滤/懒加载/目录切换时动态更新):截断 sel 到新范围 + clamp scroll。 */
|
||||
void meow_listview_set_count(meow_widget *lv, int count);
|
||||
/* 滚动:set 越界 clamp 到 [0, max_scroll];by 相对滚动(App on_swipe/touch_move 用)。 */
|
||||
void meow_listview_set_scroll(meow_widget *lv, int offset);
|
||||
void meow_listview_scroll_by(meow_widget *lv, int dy);
|
||||
int meow_listview_scroll(meow_widget *lv);
|
||||
int meow_listview_max_scroll(meow_widget *lv); /* max(0, count*item_h - viewport_h) */
|
||||
/* 选中集(mode 语义):
|
||||
* NONE :忽略;SINGLE:set_sel(idx) 设/清(idx<0 清除);MULTI:set_sel(idx) 翻转 idx。
|
||||
* get_sel 把当前选中下标写入 out[0..max),返回写入数;sel_count 返回选中总数。 */
|
||||
void meow_listview_set_sel(meow_widget *lv, int idx);
|
||||
int meow_listview_get_sel(meow_widget *lv, int *out, int max);
|
||||
int meow_listview_sel_count(meow_widget *lv);
|
||||
/* 命中:返回行号(含滚动换算;禁用行/越界返回 -1);行右 1/3 = 右侧操作区(另判)。 */
|
||||
int meow_listview_hit(meow_widget *lv, int x, int y);
|
||||
int meow_listview_hit_op(meow_widget *lv, int x, int y); /* 1=点在行右 1/3 操作区 */
|
||||
/* 行点击回调(idx=行号;op:0=行体,1=右侧操作区)。 */
|
||||
typedef void (*meow_listview_tap_fn)(int idx, int op, void *ctx);
|
||||
void meow_listview_set_on_tap(meow_widget *lv, meow_listview_tap_fn cb, void *ctx);
|
||||
/* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1。 */
|
||||
int meow_listview_tap_at(meow_widget *lv, int x, int y);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_WIDGETS_H */
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* meow_ui_zorder.h — SDK 绘制物注册表 / z 序 / 区域重画(§6.2,合并 SDK_Z_ORDER_DESIGN)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(zorder/draw 注册表)。非保留模型(§5.8.7)下,把「清底 + 按 z
|
||||
* 重画重叠区完整 z 栈」下沉到 SDK,让所有 App 自动获得正确的局部重画。全部 opt-in:
|
||||
* 不调用本节的既有 App(widget/activity/flush 均不动)行为完全不变。
|
||||
*/
|
||||
#ifndef MEOW_UI_ZORDER_H
|
||||
#define MEOW_UI_ZORDER_H
|
||||
|
||||
#include "meow_ui_types.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* §6.2 绘制物注册表 / z 序 / 区域重画(合并 SDK_Z_ORDER_DESIGN,源:2026-08-31)
|
||||
* ------------------------------------------------------------
|
||||
* 非保留模型(§5.8.7)下,把「清底 + 按 z 重画重叠区完整 z 栈」下沉到 SDK,让所有 App
|
||||
* 自动获得正确的局部重画(根治 P1:下层动态控件局部重绘会把重叠区上层 chrome 抹掉)。
|
||||
* - 控件(meow_widget)创建即自动注册进绘制物注册表;rect 实时读 w->x/y/w/h;
|
||||
* - App 直绘的非控件内容(meow_ui_text/fill_rect 等)须显式 meow_draw_custom_register
|
||||
* 注册(声明 bbox + 绘制回调),否则区域重画会漏画(非保留模型固有代价);
|
||||
* - z 自动推导(子>父、同层注册先序);显式 meow_widget_set_z(999/MEOW_Z_TOP) 置顶(粘滞)。
|
||||
* 用法:meow_ui_invalidate(x,y,w,h) 局部重画(清底+按 z 升序重画所有相交绘制物+上屏),
|
||||
* 不整帧;meow_ui_paint_full() 整帧按 z 序重画。默认底=meow_theme bg(set_page_bg 覆盖)。
|
||||
* 兼容:新 API 全部 opt-in;不调用本节的既有 App(widget/activity/flush 均不动)行为完全不变。 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 绘制物种类:控件(自动注册)/ App 直绘内容(显式注册)。 */
|
||||
typedef enum { MEOW_ITEM_WIDGET = 0, MEOW_ITEM_CUSTOM = 1 } meow_draw_kind;
|
||||
typedef void (*meow_draw_fn)(void *ctx); /* CUSTOM 绘制回调(ctx 由 App 持有) */
|
||||
|
||||
/* ---- 层级常量(保证"子>父" + 值域远小于置顶值)---- */
|
||||
#define MEOW_Z_STRIDE 16 /* 每层容器嵌套的 z 步进(子层至少 +16) */
|
||||
#define MEOW_Z_DEPTH_MAX 7 /* 自动推导最大嵌套深度(0..6,>7 折叠) */
|
||||
#define MEOW_Z_PER_LEVEL_MAX 15 /* 每层最多 16 槽(seq 0..15,超出折叠按注册序) */
|
||||
#define MEOW_Z_AUTO_MAX (MEOW_Z_STRIDE * MEOW_Z_DEPTH_MAX + MEOW_Z_PER_LEVEL_MAX) /* 127 */
|
||||
#define MEOW_Z_TOP 1000 /* 惯例"置顶"值;≥(MEOW_Z_AUTO_MAX+1)=128 即超一切自动值 */
|
||||
|
||||
/* 控件显式置层:z 为任意非负整数(0..INT_MAX,无上限),越大越上;z>所有其它控件即恒在最上。
|
||||
* 惯例设 999(或 MEOW_Z_TOP)即"置顶"。显式 z 存于绘制注册表(按 w 反查 entry,不改
|
||||
* struct meow_widget——零 ABI 破坏)且**粘滞**:不被后续自动推导覆盖;clear_z 才回退。 */
|
||||
void meow_widget_set_z(meow_widget *w, int z);
|
||||
int meow_widget_z(const meow_widget *w); /* 当前生效 z(显式=其值,否则=自动推导) */
|
||||
void meow_widget_clear_z(meow_widget *w); /* 清除显式 z,回退自动推导 */
|
||||
|
||||
/* ---- 非控件直绘内容注册为绘制物(CUSTOM)----
|
||||
* App 声明 bbox + 绘制回调 + 显式 z(任意非负;999=置顶);区域重画/整帧重画会画到它。
|
||||
* 返回 id(供 meow_draw_unregister / meow_draw_set_z);注册表满返回 0。
|
||||
* 生命周期契约:App 持有 ctx;widget 类内容不必注册(自动);离开页面前 unregister。 */
|
||||
int meow_draw_custom_register(int z, int x, int y, int w, int h,
|
||||
meow_draw_fn cb, void *ctx);
|
||||
void meow_draw_unregister(int id); /* 注销(id 失效,勿再用) */
|
||||
int meow_draw_set_z(int id, int z); /* 改 custom 显式 z;无效 id 返回 -1 */
|
||||
|
||||
/* 页面背景色(区域重画/整帧清底用);默认 meow_theme bg。 */
|
||||
void meow_ui_set_page_bg(uint32_t color);
|
||||
uint32_t meow_ui_page_bg(void);
|
||||
|
||||
/* 声明某区域内容已变化 → 清底(page_bg) + 按 z 升序重画所有相交绘制物 + 上屏。
|
||||
* 局部(只画相交绘制物,完整 bbox——见 SDK_Z_ORDER_DESIGN §5),不整帧、不冲状态栏。 */
|
||||
void meow_ui_invalidate(int x, int y, int w, int h);
|
||||
|
||||
/* 整帧按 z 序重画全部(set_page_bg 清底 + 全注册表按 z 升序 + 上屏)。 */
|
||||
void meow_ui_paint_full(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_ZORDER_H */
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* meow_notify_ipc.c — MeowOS SDK 通知 → launcher IPC 桥(任务 #5,SDK↔IPC 关键断点)。
|
||||
*
|
||||
* 让 meow_notify_send() 的通知真正经 AF_UNIX IPC 送到 launcher 中央存储(meow_notif_post)
|
||||
* 并回读 MSG_NOTIFY_ACK 拿 launcher 分配的通知 id(nid)。协议帧与负载结构复用 launcher 的
|
||||
* meow_ipc.h(同源一份、杜绝两端漂移):
|
||||
* - 帧头 16B:magic=0x4D4F5701 / version=1 / type / req_id / len(全小端 LE,mipsel 原生);
|
||||
* - MSG_NOTIFY(0x0020) ↑ 负载 = m_notify_t;MSG_NOTIFY_ACK(0x0021) ↓ 负载 = m_ack_t(status=nid);
|
||||
* - MSG_NOTIFY_DISMISS(0x0022) ↑ 负载 = uint32 nid;
|
||||
* - MSG_NOTIFY_PROGRESS(0x0024) ↑ 负载 = m_notify_progress_t(nid+progress,实时进度仅内存)。
|
||||
*
|
||||
* 设计要点:
|
||||
* - 只做「一次性 connect + send + read ACK」,不维护连接/订阅循环(与 meow_ipc.c 客户端同型);
|
||||
* - 映射层(SDK 枚举 ↔ IPC 字段)集中在本文件:分类枚举 CAT_* ↔ 字符串
|
||||
* "general"/"message"/"email"/"event"/"progress"/"media";timeout_ms → expire_timeout:
|
||||
* >0 该毫秒、0=不自动过期、-1(ONGOING)=0xFFFFFFFF(并置 ongoing=1);媒体类型置 media=1;
|
||||
* SDK `icon` → app_icon(空则 launcher 按 app_id 回退 app 图标);urgency 0-3 直通(越界钳位 NORMAL);
|
||||
* - 优雅降级:无 socket(connect 失败)/写失败/ACK 超时(SO_RCVTIMEO)/非 ACK 应答 →
|
||||
* 返回 0(调用方 meow_ui_notify.c 保留本地 id 兜底),绝不崩;
|
||||
* - 线程安全:app_id 经 pthread_once 缓存;socket 路径每次调用读 env MEOW_IPC_SOCK 覆盖
|
||||
* (宿主/test 用),缺省 meow_ipc.h 的 MEOW_IPC_PATH(/usr/data/meow_ipc.sock)。
|
||||
*
|
||||
* 约定:与 meow_ui_notify.c 同风格(M2.x 中文注释);本文件为纯传输层,不调 meow_log,
|
||||
* 失败诊断由调用方 meow_ui_notify.c 的 notify_deliver() 统一打点。
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
#include "meow_ipc.h" /* launcher 端共享协议:帧头/m_notify_t/m_ack_t/MSG_* —— 防漂移 */
|
||||
|
||||
#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>
|
||||
|
||||
/* ACK 读超时(ms):launcher 主循环为 IPC 单写者(eventfd 唤醒,正常 <10ms 响应),
|
||||
* 放宽容错 500ms;超时视为降级(返回 0,不崩)。 */
|
||||
#define MEOW_NOTIFY_IPC_TIMEOUT_MS 500
|
||||
|
||||
/* app_id(env MEOW_LOG_APP,缺省 "app";与 meow_ui_log.c 的取值口径一致)。 */
|
||||
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);
|
||||
}
|
||||
|
||||
/* socket 路径:优先 env MEOW_IPC_SOCK(宿主/qemu sim 覆盖),缺省 MEOW_IPC_PATH。 */
|
||||
static const char *ipc_sock_path(void)
|
||||
{
|
||||
const char *s = getenv("MEOW_IPC_SOCK");
|
||||
return (s && s[0]) ? s : MEOW_IPC_PATH;
|
||||
}
|
||||
|
||||
const char *meow_notify_ipc_app_id(void)
|
||||
{
|
||||
pthread_once(&g_once, ipc_init);
|
||||
return g_app_id;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 映射层: SDK meow_notify 枚举/字段 ↔ IPC m_notify_t 字段 */
|
||||
/* ============================================================ */
|
||||
|
||||
/* SDK 分类枚举 → IPC 字符串(与 meow_ui.h §9.1 的 MEOW_NOTIFY_CAT_* 序号对齐;
|
||||
* 越界钳位为 "general"——同 meow_notify_category_str 的防御语义)。 */
|
||||
static const char *cat_to_str(meow_notify_category cat)
|
||||
{
|
||||
static const char *t[] = { "general", "message", "email", "event", "progress", "media" };
|
||||
if ((int)cat >= 0 && (int)cat < (int)MEOW_NOTIFY_CAT_COUNT) return t[cat];
|
||||
return t[MEOW_NOTIFY_CAT_GENERAL];
|
||||
}
|
||||
|
||||
/* SDK timeout_ms → IPC expire_timeout(存储/显示端语义):
|
||||
* >0 → 该毫秒数后过期;0 → 不自动过期(NEVER);<0 → 常驻(ONGOING)= 0xFFFFFFFF。 */
|
||||
static uint32_t timeout_to_expire(int timeout_ms)
|
||||
{
|
||||
if (timeout_ms < 0) return 0xFFFFFFFFu; /* ONGOING:常驻,不自动过期 */
|
||||
return (uint32_t)timeout_ms; /* 0(NEVER)/ >0(毫秒) */
|
||||
}
|
||||
|
||||
/* 构建 IPC 负载 m_notify_t(app_icon<->SDK icon;urgency 0-3;actions/hints v1 留空,
|
||||
* M2b D-Bus 桥再填)。仅在持有调用者互斥之外、发送前调用(纯 CPU 拷贝,不阻塞)。 */
|
||||
static void build_m_notify(const meow_notify *n, uint32_t replaces_id, m_notify_t *mn)
|
||||
{
|
||||
memset(mn, 0, sizeof *mn);
|
||||
snprintf(mn->app_id, sizeof mn->app_id, "%s", meow_notify_ipc_app_id());
|
||||
snprintf(mn->category, sizeof mn->category, "%s", cat_to_str(n->cat));
|
||||
snprintf(mn->summary, sizeof mn->summary, "%s", n->title ? n->title : "");
|
||||
snprintf(mn->body, sizeof mn->body, "%s", n->body ? n->body : "");
|
||||
snprintf(mn->app_icon, sizeof mn->app_icon, "%s", n->icon ? n->icon : "");
|
||||
snprintf(mn->target_activity, sizeof mn->target_activity, "%s",
|
||||
n->target_activity ? n->target_activity : "");
|
||||
mn->replaces_id = replaces_id; /* 0=新建;>0=更新(不存在则落为新建,见 launcher) */
|
||||
mn->expire_timeout = timeout_to_expire(n->timeout_ms);
|
||||
mn->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress);
|
||||
mn->media = (n->cat == MEOW_NOTIFY_CAT_MEDIA) ? 1 : 0;
|
||||
mn->ongoing = (n->timeout_ms == MEOW_NOTIFY_TIMEOUT_ONGOING) ? 1 : 0;
|
||||
/* urgency:越界钳位到 NORMAL(缺省);0-3 直通(launcher 侧再回退缺省) */
|
||||
mn->urgency = ((int)n->urgency >= 0 && (int)n->urgency < (int)MEOW_NOTIFY_URG_COUNT)
|
||||
? (uint8_t)n->urgency : (uint8_t)MEOW_NOTIFY_URG_NORMAL;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 传输层(AF_UNIX SOCK_STREAM;一次性 connect + send + read) */
|
||||
/* ============================================================ */
|
||||
|
||||
/* 连接 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;
|
||||
}
|
||||
|
||||
/* 写满 n 字节(EINTR 重试);任一次写失败返回 0。 */
|
||||
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;
|
||||
}
|
||||
|
||||
/* 读满 n 字节(超时/断开返回 0)。 */
|
||||
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;
|
||||
}
|
||||
|
||||
/* 组装并写出 16B(LE)帧头 + 负载。 */
|
||||
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;
|
||||
uint16_t ver = MEOW_IPC_VERSION;
|
||||
memcpy(hdr + 0, &magic, 4);
|
||||
memcpy(hdr + 4, &ver, 2);
|
||||
memcpy(hdr + 6, &type, 2);
|
||||
memcpy(hdr + 8, &req_id, 4);
|
||||
memcpy(hdr + 12, &len, 4);
|
||||
if (!write_all(fd, hdr, sizeof hdr)) return 0;
|
||||
if (len && payload && !write_all(fd, payload, len)) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 给 conn 设 ACK 读超时(失败忽略——超时降级由 read_all 兜底)。 */
|
||||
static void set_ack_timeout(int fd)
|
||||
{
|
||||
struct timeval tv;
|
||||
tv.tv_sec = MEOW_NOTIFY_IPC_TIMEOUT_MS / 1000;
|
||||
tv.tv_usec = (MEOW_NOTIFY_IPC_TIMEOUT_MS % 1000) * 1000;
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
}
|
||||
|
||||
uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id)
|
||||
{
|
||||
m_notify_t mn;
|
||||
int fd;
|
||||
if (!n) return 0;
|
||||
build_m_notify(n, replaces_id, &mn);
|
||||
|
||||
fd = conn_path(ipc_sock_path());
|
||||
if (fd < 0) return 0; /* 无 socket:优雅降级 */
|
||||
set_ack_timeout(fd);
|
||||
|
||||
if (!send_frame(fd, MSG_NOTIFY, 0x4E4F5449u /* "NOTI" */, &mn, (uint32_t)sizeof mn)) {
|
||||
close(fd); /* 写失败:降级 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 读回 16B 应答头 + m_ack_t,取 launcher 分配的通知 id(a.status)。 */
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t nid = 0;
|
||||
if (read_all(fd, rhdr, sizeof rhdr)) {
|
||||
uint32_t rmagic, rlen;
|
||||
uint16_t rver, rtype;
|
||||
memcpy(&rmagic, rhdr + 0, 4);
|
||||
memcpy(&rver, rhdr + 4, 2);
|
||||
memcpy(&rtype, rhdr + 6, 2);
|
||||
memcpy(&rlen, rhdr + 12, 4);
|
||||
if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION &&
|
||||
rtype == MSG_NOTIFY_ACK && rlen >= sizeof(m_ack_t)) {
|
||||
uint8_t pay[sizeof(m_ack_t)];
|
||||
if (read_all(fd, pay, sizeof(m_ack_t))) {
|
||||
m_ack_t a;
|
||||
memcpy(&a, pay, sizeof a);
|
||||
if (a.code == MEOW_OK) nid = a.status; /* 落库成功 → nid */
|
||||
}
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return nid; /* 非 ACK/超时 → 0(降级) */
|
||||
}
|
||||
|
||||
void meow_notify_ipc_dismiss(uint32_t nid)
|
||||
{
|
||||
int fd;
|
||||
if (nid == 0) return;
|
||||
fd = conn_path(ipc_sock_path());
|
||||
if (fd < 0) return; /* 无 socket:静默 */
|
||||
set_ack_timeout(fd);
|
||||
if (!send_frame(fd, MSG_NOTIFY_DISMISS, 0x4E4F5444u /* "NOTD" */, &nid, (uint32_t)sizeof nid)) {
|
||||
close(fd); /* 写失败:静默 */
|
||||
return;
|
||||
}
|
||||
/* 读回应答(launcher 对 DISMISS 回 MSG_ACK,见 meow_ipc.c cmd_dismiss)。虽为
|
||||
* best-effort,但读回确保 launcher 已处理完撤下再关连接(避免写后即关的丢帧竞态)。 */
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
if (read_all(fd, rhdr, sizeof rhdr)) {
|
||||
uint32_t rmagic, rlen;
|
||||
uint16_t rver, rtype;
|
||||
memcpy(&rmagic, rhdr + 0, 4);
|
||||
memcpy(&rver, rhdr + 4, 2);
|
||||
memcpy(&rtype, rhdr + 6, 2);
|
||||
memcpy(&rlen, rhdr + 12, 4);
|
||||
if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION &&
|
||||
rtype == MSG_ACK && rlen >= sizeof(m_ack_t)) {
|
||||
uint8_t pay[sizeof(m_ack_t)];
|
||||
(void)read_all(fd, pay, sizeof(m_ack_t)); /* 读到即确认;内容不关心 */
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
void meow_notify_ipc_update_progress(uint32_t nid, int progress)
|
||||
{
|
||||
int fd;
|
||||
if (nid == 0) return;
|
||||
m_notify_progress_t p;
|
||||
memset(&p, 0, sizeof p);
|
||||
p.nid = nid;
|
||||
p.progress = (progress < -1) ? -1 : (progress > 100 ? 100 : progress);
|
||||
fd = conn_path(ipc_sock_path());
|
||||
if (fd < 0) return; /* 无 socket:静默 */
|
||||
set_ack_timeout(fd);
|
||||
if (!send_frame(fd, MSG_NOTIFY_PROGRESS, 0x4E4F5450u /* "NOTP" */, &p, (uint32_t)sizeof p)) {
|
||||
close(fd); /* 写失败:静默 */
|
||||
return;
|
||||
}
|
||||
/* 读回应答(launcher 对 PROGRESS 回 MSG_ACK,best-effort;读回确保 launcher 已处理完) */
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
if (read_all(fd, rhdr, sizeof rhdr)) {
|
||||
uint32_t rmagic, rlen;
|
||||
uint16_t rver, rtype;
|
||||
memcpy(&rmagic, rhdr + 0, 4);
|
||||
memcpy(&rver, rhdr + 4, 2);
|
||||
memcpy(&rtype, rhdr + 6, 2);
|
||||
memcpy(&rlen, rhdr + 12, 4);
|
||||
if (rmagic == MEOW_IPC_MAGIC && rver == MEOW_IPC_VERSION &&
|
||||
rtype == MSG_ACK && rlen >= sizeof(m_ack_t)) {
|
||||
uint8_t pay[sizeof(m_ack_t)];
|
||||
(void)read_all(fd, pay, sizeof(m_ack_t)); /* 读到即确认;内容不关心 */
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* ---- 通用一次性帧(meow_ui_nav / notif_open 复用本传输层) ---- */
|
||||
int meow_notify_ipc_send_raw(uint16_t type, const void *payload, uint32_t len, int want_ack)
|
||||
{
|
||||
int fd = conn_path(ipc_sock_path());
|
||||
if (fd < 0) return -1;
|
||||
set_ack_timeout(fd);
|
||||
if (!send_frame(fd, type, 0, payload, len)) { close(fd); return -1; }
|
||||
if (want_ack) {
|
||||
/* 阻塞读一帧应答头(best-effort:读到即确认 launcher 已处理;内容不关心)。
|
||||
* 超时(SO_RCVTIMEO)/断开 → read_all 返回 0,视为已发出(不崩)。 */
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
if (read_all(fd, rhdr, sizeof rhdr)) {
|
||||
/* 读完应答负载(若声明),保持连接语义一致;不校验 magic(best-effort) */
|
||||
uint32_t rlen;
|
||||
memcpy(&rlen, rhdr + 12, 4);
|
||||
if (rlen > 0 && rlen <= MEOW_IPC_MAX) {
|
||||
uint8_t pay[256];
|
||||
size_t to = rlen < sizeof pay ? rlen : sizeof pay;
|
||||
(void)read_all(fd, pay, to);
|
||||
}
|
||||
}
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void meow_notify_ipc_notif_open(const char *app_id, uint32_t event_seq,
|
||||
uint16_t origin_x, uint16_t origin_y,
|
||||
uint16_t hotzone, uint8_t flags)
|
||||
{
|
||||
m_notif_open_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "%s",
|
||||
(app_id && app_id[0]) ? app_id : meow_notify_ipc_app_id());
|
||||
o.event_seq = event_seq;
|
||||
o.origin_x = origin_x;
|
||||
o.origin_y = origin_y;
|
||||
o.hotzone = hotzone;
|
||||
o.flags = flags;
|
||||
(void)meow_notify_ipc_send_raw(MSG_NOTIF_OPEN, &o, (uint32_t)sizeof o, 1);
|
||||
}
|
||||
|
||||
void meow_notify_ipc_notif_close(const char *app_id, uint32_t reason)
|
||||
{
|
||||
m_notif_close_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "%s",
|
||||
(app_id && app_id[0]) ? app_id : meow_notify_ipc_app_id());
|
||||
o.reason = reason;
|
||||
(void)meow_notify_ipc_send_raw(MSG_NOTIF_CLOSE, &o, (uint32_t)sizeof o, 1);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* meow_notify_ipc.h — MeowOS SDK 通知 → launcher IPC 桥(内部头,不随 meow_ui.h 发布给应用)。
|
||||
*
|
||||
* 定位(任务 #5 SDK↔IPC 桥关键断点):让框架 app 调 meow_notify_send() 的通知真正经
|
||||
* AF_UNIX IPC 送到 launcher(meow_launcherd)中央存储(meow_notif.c 的 meow_notif_post),
|
||||
* 通知中心(阶段 B · M5)才能显示。消息通道(FRAMEWORK_DESIGN §1 / meow_ipc.h):
|
||||
* - MSG_NOTIFY 0x0020 ↑:推送/更新通知(负载 m_notify_t;含 SDK icon→app_icon、urgency);
|
||||
* - MSG_NOTIFY_ACK 0x0021 ↓:落库结果(m_ack_t,status=launcher 分配的通知 id nid);
|
||||
* - MSG_NOTIFY_DISMISS 0x0022 ↑:撤下通知(负载 uint32 nid);
|
||||
* - MSG_NOTIFY_PROGRESS 0x0024 ↑:实时更新通知进度(负载 m_notify_progress_t,仅改内存活跃槽)。
|
||||
*
|
||||
* 约定:只做「一次性发送 + 读 ACK」,不造 IPC 订阅循环;SDK 本地活动表(meow_ui_notify.c)
|
||||
* 保留(app 进程内即时查询/配额自检),真正的通知中心数据在 launcher 中央存储,二者以
|
||||
* 本接口经 IPC 同步。全部失败路径优雅降级(无 socket/写失败/超时 → 返回 0,不崩)。
|
||||
*
|
||||
* 本接口只暴露「SDK 类型出/进 + 状态码」,不把 m_notify_t / 帧格式泄漏给 meow_ui_notify.c;
|
||||
* 映射层(SDK 枚举 ↔ IPC 字符串/uint32)实现在 meow_notify_ipc.c 内部。
|
||||
*/
|
||||
#ifndef MEOW_NOTIFY_IPC_H
|
||||
#define MEOW_NOTIFY_IPC_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "meow_ui.h" /* meow_notify */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 当前 app 的 app_id(env MEOW_LOG_APP,缺省 "app";pthread_once 缓存,线程安全)。 */
|
||||
const char *meow_notify_ipc_app_id(void);
|
||||
|
||||
/* 把一条 SDK meow_notify 映射为 m_notify_t 并一次性发送 MSG_NOTIFY,阻塞读
|
||||
* MSG_NOTIFY_ACK 取 launcher 分配的通知 id(nid)。replaces_id:0=新建;>0=更新已存在
|
||||
* 通知(与 freedesktop 语义一致)。成功返回 nid(>0);无 socket/写失败/超时/非 ACK →
|
||||
* 返回 0(调用方保留本地 id 兜底,不崩)。 */
|
||||
uint32_t meow_notify_ipc_send(const meow_notify *n, uint32_t replaces_id);
|
||||
|
||||
/* 一次性发送 MSG_NOTIFY_DISMISS 撤下 launcher 侧通知。best-effort:写失败/无 socket
|
||||
* 静默(不崩、不回读 ACK)。 */
|
||||
void meow_notify_ipc_dismiss(uint32_t nid);
|
||||
|
||||
/* 一次性发送 MSG_NOTIFY_PROGRESS 更新 launcher 侧已存在通知的进度(仅改内存活跃槽,
|
||||
* 不落盘)。best-effort:写失败/无 socket 静默。 */
|
||||
void meow_notify_ipc_update_progress(uint32_t nid, int progress);
|
||||
|
||||
/* 通用一次性请求:发 type+payload 帧,want_ack 时阻塞读一帧应答(忽略其内容,仅确保
|
||||
* launcher 已处理完再关连接,防「写后即关」丢帧竞态)。返回 0=已发出(want_ack 含读到应答);
|
||||
* -1=无 socket/写失败/读失败。best-effort,供 SDK 内部 nav / notif_open 复用。 */
|
||||
int meow_notify_ipc_send_raw(uint16_t type, const void *payload, uint32_t len, int want_ack);
|
||||
|
||||
/* 一次性发送 MSG_NOTIF_OPEN(App 前台顶部下滑 → 请求 launcher 展开通知中心叠加会话,
|
||||
* FRAMEWORK_DESIGN §10.5)。App 只检测+上报、不渲染系统 UI。best-effort:失败静默。 */
|
||||
void meow_notify_ipc_notif_open(const char *app_id, uint32_t event_seq,
|
||||
uint16_t origin_x, uint16_t origin_y,
|
||||
uint16_t hotzone, uint8_t flags);
|
||||
|
||||
/* 一次性发送 MSG_NOTIF_CLOSE(App 告知结束/退出 → launcher 收尾兜底,可选用途)。best-effort。 */
|
||||
void meow_notify_ipc_notif_close(const char *app_id, uint32_t reason);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_NOTIFY_IPC_H */
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* meow_pinyin.c — 中文拼音Ⅰ级候选引擎(静态表驱动,纯 C,零堆)。
|
||||
*
|
||||
* 词库在 meow_pinyin_lex.h(gen_pinyin_lex.py 离线生成,.rodata)。
|
||||
* 供 meow_ui_ime.c(MEOW_IME_PINYIN 模式)调用;SDK 集成时本 TU 进 meow_ui.a。
|
||||
*
|
||||
* 查询 = 当前组合拼音串(去声调小写,如 "nihao")→ 候选列表(UTF-8)。
|
||||
* 排序规则(与设计稿 §4.4 / /tmp/pyim engine.c 一致):
|
||||
* 1) 短语精确命中(sp==输入,如 nihao→你好)最优先;
|
||||
* 2) 音节切分组合(ni+hao→你好/你号/呢好…)按「每音节候选字序号之和」升序;
|
||||
* 3) 前缀整词提示(sp 以输入为前缀,如 ni→你好/你好吗)排最后(边打边出整词)。
|
||||
*
|
||||
* 设计约束:无 stdio(拉体积),无动态分配(MIPS32/14MB 意识)。切分 DFS + 组合
|
||||
* 上限常量封死(MAXSPLIT/MAXCOMB),防长组合串爆炸。词表按 sp 前缀匹配(线性回溯)。
|
||||
*/
|
||||
#include "meow_pinyin_lex.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define MAXCAND 32 /* 每轮候选上限(与 IME 候选数组对齐) */
|
||||
#define MAXSPLIT 64 /* 切分枚举上限 */
|
||||
#define MAXSYL 8 /* 一次组合的音节数上限 */
|
||||
#define TK 4 /* 每音节取前 K 个字符再组合(限组合数) */
|
||||
#define MAXCOMB 120 /* 笛卡尔组合 guard(防爆炸) */
|
||||
|
||||
typedef struct { char hz[64]; int score; int src; int seg; } cand; /* src: 0=短语 1=切分组合 2=前缀词提示; seg=切分音节数 */
|
||||
|
||||
/* 定长拷贝一个 NUL 结尾字符串到 d[64];用 memcpy 规避 -Wstringop-truncation(来源可能超 63)。 */
|
||||
static void set_hz(char *d, const char *s)
|
||||
{
|
||||
size_t n = strlen(s);
|
||||
if (n > 63) n = 63;
|
||||
memcpy(d, s, n);
|
||||
d[n] = 0;
|
||||
}
|
||||
|
||||
/* 收集某音节候选字(UTF-8 单字),填入 chars[][4](UTF-8 ≤3B + NUL)。返回字数。 */
|
||||
static int syl_chars(int s, char chars[][4], int maxk)
|
||||
{
|
||||
const char *h = SYL[s].hz;
|
||||
int n = 0, i = 0;
|
||||
while (h[i] && n < maxk) {
|
||||
int l = 1;
|
||||
if ((unsigned char)h[i] >= 0xF0) l = 4;
|
||||
else if ((unsigned char)h[i] >= 0xE0) l = 3;
|
||||
else if ((unsigned char)h[i] >= 0xC0) l = 2;
|
||||
if (n < maxk) { memcpy(chars[n], h + i, (size_t)l); chars[n][l] = 0; n++; }
|
||||
i += l;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 枚举 py 全部合法音节切分(DFS)。g_split[i]=切分路径(音节下标数组)。 */
|
||||
static int g_split[MAXSPLIT][MAXSYL];
|
||||
static int g_splitlen[MAXSPLIT];
|
||||
static int g_ns;
|
||||
|
||||
static void dfs_split(const char *py, int i, int path[], int plen)
|
||||
{
|
||||
if (g_ns >= MAXSPLIT) return;
|
||||
if (py[i] == 0) {
|
||||
if (plen == 0) return;
|
||||
memcpy(g_split[g_ns], path, (size_t)plen * sizeof(int));
|
||||
g_splitlen[g_ns] = plen;
|
||||
g_ns++;
|
||||
return;
|
||||
}
|
||||
for (int s = 0; s < SYL_N && g_ns < MAXSPLIT; s++) {
|
||||
int l = (int)strlen(SYL[s].py);
|
||||
if (strncmp(py + i, SYL[s].py, (size_t)l) == 0) {
|
||||
path[plen] = s;
|
||||
dfs_split(py, i + l, path, plen + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int cmp_cand(const void *a, const void *b)
|
||||
{
|
||||
const cand *x = (const cand *)a, *y = (const cand *)b;
|
||||
if (x->score != y->score) return x->score - y->score; /* 升序:score 越小越常用 */
|
||||
if (x->seg != y->seg) return x->seg - y->seg; /* score 相同:音节数越少越优先(完整音节读法) */
|
||||
return x->src - y->src;
|
||||
}
|
||||
|
||||
static void dedupe(cand *c, int *n)
|
||||
{
|
||||
for (int i = 0; i < *n; i++)
|
||||
for (int j = i + 1; j < *n; j++)
|
||||
if (strcmp(c[i].hz, c[j].hz) == 0) { c[j].score = 99999; }
|
||||
int w = 0;
|
||||
for (int i = 0; i < *n; i++) if (c[i].score != 99999) c[w++] = c[i];
|
||||
*n = w;
|
||||
}
|
||||
|
||||
/* 主入口:输入当前拼音组合串(去声调),输出候选列表(UTF-8),返回个数。
|
||||
* out 至少 maxout 项,每项定长 64B(拷贝,非持久)。 */
|
||||
int py_predict(const char *py, char out[][64], int maxout)
|
||||
{
|
||||
cand cs[MAXCAND]; int nc = 0;
|
||||
const int len = (int)strlen(py);
|
||||
if (len == 0 || maxout <= 0) return 0;
|
||||
if (maxout > MAXCAND) maxout = MAXCAND;
|
||||
|
||||
/* 1) 短语精确命中(band 0:最优先)。词表可能多条同拼(如 'shi'/'dui' 单字词),按序给 rank。 */
|
||||
int exact_rank = 0;
|
||||
for (int i = 0; i < WORD_N && nc < MAXCAND; i++) {
|
||||
if (strcmp(WORDS[i].sp, py) == 0) {
|
||||
set_hz(cs[nc].hz, WORDS[i].hz);
|
||||
cs[nc].score = exact_rank++;
|
||||
cs[nc].src = 0; cs[nc].seg = 1; nc++;
|
||||
}
|
||||
}
|
||||
/* 2) 前缀词提示(band 2:边打边出整词,排在单字组合之后) */
|
||||
int pre_rank = 0;
|
||||
for (int i = 0; i < WORD_N && nc < MAXCAND; i++) {
|
||||
int wl = (int)strlen(WORDS[i].sp);
|
||||
if (wl > len && strncmp(WORDS[i].sp, py, (size_t)len) == 0) {
|
||||
set_hz(cs[nc].hz, WORDS[i].hz);
|
||||
cs[nc].score = 200 + pre_rank++;
|
||||
cs[nc].src = 2; cs[nc].seg = 1; nc++;
|
||||
}
|
||||
}
|
||||
/* 3) 音节切分组合 */
|
||||
g_ns = 0;
|
||||
{ int path[MAXSYL]; dfs_split(py, 0, path, 0); }
|
||||
for (int s = 0; s < g_ns && nc < MAXCAND; s++) {
|
||||
const int *seg = g_split[s];
|
||||
int nseg = g_splitlen[s];
|
||||
char cset[MAXSYL][TK][4];
|
||||
int cnt[MAXSYL];
|
||||
for (int k = 0; k < nseg; k++) cnt[k] = syl_chars(seg[k], cset[k], TK);
|
||||
int iter[MAXSYL]; for (int k = 0; k < nseg; k++) iter[k] = 0;
|
||||
int guard = 0;
|
||||
while (guard++ < MAXCOMB && nc < MAXCAND) {
|
||||
char buf[64]; buf[0] = 0; int score = 0;
|
||||
for (int k = 0; k < nseg; k++) { strcat(buf, cset[k][iter[k]]); score += iter[k]; }
|
||||
if (buf[0]) {
|
||||
set_hz(cs[nc].hz, buf);
|
||||
cs[nc].score = 100 + score; /* band 1:切分组合(sum of char index 越小越常用) */
|
||||
cs[nc].src = 1; cs[nc].seg = nseg; nc++;
|
||||
}
|
||||
int k = nseg - 1;
|
||||
while (k >= 0) {
|
||||
iter[k]++;
|
||||
if (iter[k] < cnt[k]) break;
|
||||
iter[k] = 0; k--;
|
||||
}
|
||||
if (k < 0) break;
|
||||
}
|
||||
}
|
||||
dedupe(cs, &nc);
|
||||
qsort(cs, (size_t)nc, sizeof(cand), cmp_cand);
|
||||
if (nc > maxout) nc = maxout;
|
||||
for (int i = 0; i < nc; i++) { memcpy(out[i], cs[i].hz, 64); }
|
||||
return nc;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* meow_pinyin.h — 中文拼音候选引擎公开接口(meow_ui_ime.c 调用;实现在 meow_pinyin.c)。
|
||||
* 词库(meow_pinyin_lex.h)与本接口均进 meow_ui.a。纯 C 静态表驱动,零堆、无 stdio。
|
||||
*/
|
||||
#ifndef MEOW_PINYIN_H
|
||||
#define MEOW_PINYIN_H
|
||||
|
||||
/* 输入当前拼音组合串(去声调小写,如 "nihao"),输出候选列表(UTF-8,out 拷贝),返回条数。
|
||||
* out 至少 maxout 项、每项 ≥64B。无候选返回 0。 */
|
||||
int py_predict(const char *py, char out[][64], int maxout);
|
||||
|
||||
#endif /* MEOW_PINYIN_H */
|
||||
@@ -0,0 +1,750 @@
|
||||
/* 自动生成 by gen_pinyin_lex.py —— 中文拼音→候选字/词 频序表(去声调,UTF-8,.rodata)。*/
|
||||
/* 词库规模:音节 403 条 + 词语 329 条;候选字全部落在 GB2312 常用字集(common/gbk tier)。 */
|
||||
#ifndef MEOW_PINYIN_LEX_H
|
||||
#define MEOW_PINYIN_LEX_H
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct { const char *py; const char *hz; } py_syl;
|
||||
typedef struct { const char *sp; const char *hz; int w; } py_word;
|
||||
|
||||
#define SYL_N 403
|
||||
static const py_syl SYL[SYL_N] = {
|
||||
{"a", "啊阿吖呵腌"},
|
||||
{"ai", "爱哀挨唉矮碍癌艾呆埃"},
|
||||
{"an", "安按暗案岸俺鞍庵"},
|
||||
{"ang", "昂盎肮"},
|
||||
{"ao", "奥懊傲熬凹澳敖袄"},
|
||||
{"ba", "把八吧爸拔罢霸巴扒芭"},
|
||||
{"bai", "白百摆败拜柏伯佰"},
|
||||
{"ban", "办半班搬版板伴扮拌瓣"},
|
||||
{"bang", "帮棒榜绑膀磅邦蚌"},
|
||||
{"bao", "报保包宝抱暴薄爆饱堡"},
|
||||
{"bei", "被北背倍杯备悲辈贝碑"},
|
||||
{"ben", "本奔笨苯"},
|
||||
{"beng", "蹦崩甭泵迸"},
|
||||
{"bi", "比必笔闭毕鼻避彼壁弊"},
|
||||
{"bian", "变边便遍辨编辩扁鞭"},
|
||||
{"biao", "表标彪镖膘"},
|
||||
{"bie", "别憋鳖瘪"},
|
||||
{"bin", "宾滨彬濒斌"},
|
||||
{"bing", "病并兵冰饼丙柄秉"},
|
||||
{"bo", "波播拨薄博伯驳脖膊"},
|
||||
{"bu", "不步部布补捕怖埠簿"},
|
||||
{"ca", "擦嚓"},
|
||||
{"cai", "才菜采财猜彩裁踩材"},
|
||||
{"can", "参残餐灿惨蚕惭"},
|
||||
{"cang", "藏仓苍舱"},
|
||||
{"cao", "草操曹槽糙"},
|
||||
{"ce", "侧测策册厕"},
|
||||
{"cen", "岑"},
|
||||
{"ceng", "层曾蹭"},
|
||||
{"cha", "查差茶插叉察刹诧"},
|
||||
{"chai", "拆柴差钗"},
|
||||
{"chan", "产缠颤馋禅蝉"},
|
||||
{"chang", "长常场厂唱昌畅肠尝偿"},
|
||||
{"chao", "超朝潮吵炒抄钞巢"},
|
||||
{"che", "车扯撤彻澈"},
|
||||
{"chen", "陈沉晨臣尘趁衬辰"},
|
||||
{"cheng", "成城程称承诚乘盛橙惩"},
|
||||
{"chi", "吃持池迟赤尺斥驰齿翅"},
|
||||
{"chong", "冲重虫充宠崇憧"},
|
||||
{"chou", "抽愁丑臭筹仇酬绸"},
|
||||
{"chu", "出处初除楚厨触储础畜"},
|
||||
{"chua", "欻"},
|
||||
{"chuai", "揣踹"},
|
||||
{"chuan", "传船穿川串喘"},
|
||||
{"chuang", "创窗床闯疮"},
|
||||
{"chui", "吹垂锤炊"},
|
||||
{"chun", "春纯唇蠢淳"},
|
||||
{"chuo", "戳绰辍"},
|
||||
{"ci", "次此词刺辞瓷慈磁雌"},
|
||||
{"cong", "从聪葱囱匆"},
|
||||
{"cou", "凑"},
|
||||
{"cu", "粗促醋簇卒"},
|
||||
{"cuan", "窜窜蹿"},
|
||||
{"cui", "催脆翠摧崔粹"},
|
||||
{"cun", "存村寸蹲"},
|
||||
{"cuo", "错措搓挫磋"},
|
||||
{"da", "大打达答搭沓嗒"},
|
||||
{"dai", "带代待戴袋呆逮贷岱"},
|
||||
{"dan", "但担单弹蛋淡胆旦耽诞"},
|
||||
{"dang", "当挡党荡档挡"},
|
||||
{"dao", "到道倒刀岛导盗稻悼"},
|
||||
{"de", "的得地德"},
|
||||
{"deng", "等灯登邓凳瞪"},
|
||||
{"di", "地第低底敌抵滴弟帝递"},
|
||||
{"dian", "点电店典殿掂惦奠"},
|
||||
{"diao", "调掉钓雕叼吊"},
|
||||
{"die", "跌爹叠碟蝶迭"},
|
||||
{"ding", "定顶丁订钉盯鼎"},
|
||||
{"diu", "丢"},
|
||||
{"dong", "动东懂冬洞冻董栋"},
|
||||
{"dou", "都斗豆逗兜陡抖"},
|
||||
{"du", "度读独毒肚堵赌渡杜"},
|
||||
{"duan", "段断短端锻"},
|
||||
{"dui", "对队堆兑"},
|
||||
{"dun", "顿吨蹲盾钝敦"},
|
||||
{"duo", "多朵夺躲堕舵跺"},
|
||||
{"e", "饿恶额鹅俄蛾遏鄂"},
|
||||
{"en", "恩摁"},
|
||||
{"er", "而二儿耳尔饵洱"},
|
||||
{"fa", "发法罚伐乏阀筏"},
|
||||
{"fan", "反饭犯翻凡烦返泛范繁"},
|
||||
{"fang", "方放房防访仿坊纺"},
|
||||
{"fei", "非飞费肥废匪肺菲"},
|
||||
{"fen", "分粉份奋愤纷芬坟焚"},
|
||||
{"feng", "风封峰丰疯逢缝蜂凤"},
|
||||
{"fo", "佛"},
|
||||
{"fou", "否"},
|
||||
{"fu", "副复福扶服府负富付妇父符附腐"},
|
||||
{"ga", "嘎尬噶"},
|
||||
{"gai", "该改盖概钙溉"},
|
||||
{"gan", "干感敢赶甘杆肝竿"},
|
||||
{"gang", "刚钢港岗缸杠"},
|
||||
{"gao", "高告搞稿糕膏"},
|
||||
{"ge", "个各歌格哥隔割阁胳"},
|
||||
{"gei", "给"},
|
||||
{"gen", "跟根跟"},
|
||||
{"geng", "更耕颈梗耿"},
|
||||
{"gong", "公共工功攻供宫拱贡恭"},
|
||||
{"gou", "够狗购勾沟构钩"},
|
||||
{"gu", "故古顾股骨鼓谷姑孤估"},
|
||||
{"gua", "挂瓜刮寡卦"},
|
||||
{"guai", "怪拐乖"},
|
||||
{"guan", "关管观官冠馆惯罐"},
|
||||
{"guang", "光广逛"},
|
||||
{"gui", "贵归鬼规柜桂跪诡"},
|
||||
{"gun", "滚棍"},
|
||||
{"guo", "国过果锅裹郭"},
|
||||
{"ha", "哈蛤"},
|
||||
{"hai", "还海害孩嗨骇"},
|
||||
{"han", "含喊汉汗寒韩罕憾"},
|
||||
{"hang", "行航夯杭"},
|
||||
{"hao", "好号浩豪毫郝耗昊"},
|
||||
{"he", "和合何河喝盒禾鹤贺荷"},
|
||||
{"hei", "黑嘿"},
|
||||
{"hen", "很恨狠痕"},
|
||||
{"heng", "横恒衡哼"},
|
||||
{"hong", "红洪宏虹烘鸿"},
|
||||
{"hou", "后候厚猴喉吼"},
|
||||
{"hu", "湖户呼胡虎糊互忽壶糊"},
|
||||
{"hua", "话花化画华滑划桦"},
|
||||
{"huai", "坏怀淮"},
|
||||
{"huan", "还换环欢缓幻患唤"},
|
||||
{"huang", "黄慌皇荒晃煌幌"},
|
||||
{"hui", "会回挥灰汇毁惠慧辉恢悔"},
|
||||
{"hun", "混昏婚魂浑"},
|
||||
{"huo", "或火活货获伙惑霍"},
|
||||
{"ji", "及机级极即急集计记既济季几给基激肌迹际剂"},
|
||||
{"jia", "家加价假架甲佳嘉嫁夹"},
|
||||
{"jian", "见件间建减检简坚健剑渐尖肩荐监"},
|
||||
{"jiang", "将讲江奖降酱姜僵"},
|
||||
{"jiao", "叫教交角觉较脚焦骄娇郊浇"},
|
||||
{"jie", "界接解节姐借街杰皆截戒届阶"},
|
||||
{"jin", "进近金今紧仅尽禁劲津筋"},
|
||||
{"jing", "经精京静境警惊景镜竟竞睛晶"},
|
||||
{"jiong", "窘炯"},
|
||||
{"jiu", "就九久酒旧救究舅揪"},
|
||||
{"ju", "据居具局举句巨距聚拒剧"},
|
||||
{"juan", "卷捐娟倦眷"},
|
||||
{"jue", "觉决绝角掘爵倔嚼"},
|
||||
{"jun", "军均君俊菌骏"},
|
||||
{"ka", "卡咖喀"},
|
||||
{"kai", "开凯慨楷"},
|
||||
{"kan", "看砍刊堪坎勘"},
|
||||
{"kang", "抗康炕扛慷"},
|
||||
{"kao", "考靠烤拷"},
|
||||
{"ke", "可课科克客刻渴棵颗咳"},
|
||||
{"kei", "剋"},
|
||||
{"ken", "肯垦啃恳"},
|
||||
{"keng", "坑吭"},
|
||||
{"kong", "空控孔恐"},
|
||||
{"kou", "口扣抠寇"},
|
||||
{"ku", "苦哭库裤酷枯窟"},
|
||||
{"kua", "跨夸垮挎"},
|
||||
{"kuai", "快块筷会脍"},
|
||||
{"kuan", "宽款"},
|
||||
{"kuang", "况狂矿框筐旷"},
|
||||
{"kui", "亏溃愧窥葵"},
|
||||
{"kun", "困昆捆坤"},
|
||||
{"kuo", "扩阔括廓"},
|
||||
{"la", "拉啦辣蜡腊喇"},
|
||||
{"lai", "来莱赖"},
|
||||
{"lan", "蓝栏拦懒烂览兰篮"},
|
||||
{"lang", "浪狼朗郎"},
|
||||
{"lao", "老劳牢捞唠姥"},
|
||||
{"le", "了乐勒"},
|
||||
{"lei", "类累泪雷蕾垒"},
|
||||
{"leng", "冷愣棱"},
|
||||
{"li", "里力理立利李离例历丽励粒厉礼"},
|
||||
{"lia", "俩"},
|
||||
{"lian", "连脸练联恋链帘莲"},
|
||||
{"liang", "量两亮辆凉粮良梁"},
|
||||
{"liao", "了料聊疗辽廖"},
|
||||
{"lie", "列烈裂猎劣"},
|
||||
{"lin", "林临邻淋琳磷"},
|
||||
{"ling", "令领另零灵铃岭凌"},
|
||||
{"liu", "六流留刘柳溜瘤"},
|
||||
{"long", "龙隆笼聋拢垄"},
|
||||
{"lou", "楼漏搂陋"},
|
||||
{"lu", "路陆录露鲁卢炉鹿炉"},
|
||||
{"luan", "乱卵峦"},
|
||||
{"lun", "论轮伦沦"},
|
||||
{"lue", "略掠"},
|
||||
{"luo", "落罗洛络骆裸"},
|
||||
{"lv", "率律绿虑旅驴吕铝"},
|
||||
{"lve", "略掠"},
|
||||
{"ma", "吗妈马麻骂码嘛玛"},
|
||||
{"mai", "买卖麦埋迈脉"},
|
||||
{"man", "满慢蛮瞒漫曼"},
|
||||
{"mang", "忙茫盲芒莽"},
|
||||
{"mao", "毛猫冒帽茂矛贸锚"},
|
||||
{"me", "么"},
|
||||
{"mei", "没每美妹梅眉媒霉"},
|
||||
{"men", "们门闷萌"},
|
||||
{"meng", "梦猛蒙盟萌"},
|
||||
{"mi", "米密迷秘蜜谜觅"},
|
||||
{"mian", "面免棉眠缅勉"},
|
||||
{"miao", "秒妙苗描庙瞄"},
|
||||
{"mie", "灭蔑"},
|
||||
{"min", "民敏闽皿"},
|
||||
{"ming", "明名命鸣铭茗"},
|
||||
{"miu", "谬"},
|
||||
{"mo", "摸模磨末莫默墨陌摩"},
|
||||
{"mou", "某谋牟"},
|
||||
{"mu", "目母木幕牧慕墓穆亩"},
|
||||
{"na", "那拿哪纳娜呐"},
|
||||
{"nai", "奶乃耐奈"},
|
||||
{"nan", "难南男楠"},
|
||||
{"nang", "囊"},
|
||||
{"nao", "脑闹恼挠"},
|
||||
{"ne", "呢"},
|
||||
{"nei", "内馁"},
|
||||
{"nen", "嫩"},
|
||||
{"neng", "能"},
|
||||
{"ni", "你呢泥尼妮霓逆拟腻匿"},
|
||||
{"nian", "年念粘捻撵"},
|
||||
{"niang", "娘酿"},
|
||||
{"niao", "鸟尿"},
|
||||
{"nie", "捏聂孽"},
|
||||
{"nin", "您"},
|
||||
{"ning", "宁凝拧"},
|
||||
{"niu", "牛扭纽妞"},
|
||||
{"nong", "弄农浓脓"},
|
||||
{"nou", "耨"},
|
||||
{"nu", "怒努奴"},
|
||||
{"nuan", "暖"},
|
||||
{"nuo", "诺挪懦"},
|
||||
{"nv", "女"},
|
||||
{"nve", "虐疟"},
|
||||
{"o", "哦噢"},
|
||||
{"ou", "欧偶呕殴藕"},
|
||||
{"pa", "怕爬趴帕啪"},
|
||||
{"pai", "排派拍牌徘湃"},
|
||||
{"pan", "判盘盼攀畔叛"},
|
||||
{"pang", "旁胖庞乓"},
|
||||
{"pao", "跑炮泡抛袍"},
|
||||
{"pei", "陪配佩赔培"},
|
||||
{"pen", "喷盆"},
|
||||
{"peng", "朋碰捧棚蓬鹏"},
|
||||
{"pi", "皮批脾披疲劈匹屁"},
|
||||
{"pian", "片篇偏骗"},
|
||||
{"piao", "票飘漂瓢"},
|
||||
{"pie", "撇瞥"},
|
||||
{"pin", "品拼贫频聘"},
|
||||
{"ping", "平评凭瓶苹萍乒"},
|
||||
{"po", "破坡婆迫颇魄"},
|
||||
{"pou", "剖"},
|
||||
{"pu", "铺普朴谱浦葡仆"},
|
||||
{"qi", "起期其气七齐器汽奇乞旗棋"},
|
||||
{"qia", "恰掐洽"},
|
||||
{"qian", "前钱千签浅欠牵铅迁谦"},
|
||||
{"qiang", "强抢墙枪腔"},
|
||||
{"qiao", "桥瞧敲俏巧悄壳"},
|
||||
{"qie", "且切窃茄"},
|
||||
{"qin", "亲琴勤侵秦"},
|
||||
{"qing", "请清轻青情晴顷倾"},
|
||||
{"qiong", "穷琼"},
|
||||
{"qiu", "求球秋丘邱"},
|
||||
{"qu", "去取区曲娶趣趋渠"},
|
||||
{"quan", "全权圈劝拳泉犬"},
|
||||
{"que", "却确缺雀鹊"},
|
||||
{"qun", "群裙"},
|
||||
{"ran", "然染燃"},
|
||||
{"rang", "让嚷壤"},
|
||||
{"rao", "绕饶扰"},
|
||||
{"re", "热惹"},
|
||||
{"ren", "人认任仁忍韧刃"},
|
||||
{"reng", "仍扔"},
|
||||
{"ri", "日"},
|
||||
{"rong", "容荣融绒溶"},
|
||||
{"rou", "肉柔揉"},
|
||||
{"ru", "如入辱儒乳汝"},
|
||||
{"rua", "挼"},
|
||||
{"ruan", "软"},
|
||||
{"rui", "瑞锐"},
|
||||
{"run", "润闰"},
|
||||
{"ruo", "若弱"},
|
||||
{"sa", "撒洒萨"},
|
||||
{"sai", "赛塞腮"},
|
||||
{"san", "三散伞"},
|
||||
{"sang", "桑嗓丧"},
|
||||
{"sao", "扫嫂骚"},
|
||||
{"se", "色涩瑟"},
|
||||
{"sen", "森"},
|
||||
{"seng", "僧"},
|
||||
{"sha", "沙杀傻纱啥刹煞"},
|
||||
{"shai", "晒筛"},
|
||||
{"shan", "山善闪衫删扇陕"},
|
||||
{"shang", "上商伤尚赏裳"},
|
||||
{"shao", "少烧稍绍捎勺"},
|
||||
{"she", "社设舍射蛇摄涉奢"},
|
||||
{"shen", "身深神甚伸什申慎渗"},
|
||||
{"sheng", "生声省胜圣升盛剩"},
|
||||
{"shi", "是事时十式市师世史示使士失视实识施"},
|
||||
{"shou", "手受收首守售寿授瘦"},
|
||||
{"shu", "书数术树输属舒叔殊疏蔬"},
|
||||
{"shua", "刷耍"},
|
||||
{"shuai", "帅摔甩衰"},
|
||||
{"shuan", "栓拴"},
|
||||
{"shuang", "双爽"},
|
||||
{"shui", "水睡税"},
|
||||
{"shun", "顺瞬"},
|
||||
{"shuo", "说硕烁"},
|
||||
{"si", "四死思斯司私似撕寺饲"},
|
||||
{"song", "送松宋诵颂"},
|
||||
{"sou", "搜艘嗽"},
|
||||
{"su", "素速苏诉宿俗塑肃"},
|
||||
{"suan", "算酸蒜"},
|
||||
{"sui", "岁随虽碎遂穗"},
|
||||
{"sun", "孙损笋"},
|
||||
{"suo", "所锁索缩嗦"},
|
||||
{"ta", "他她它塔踏塌拓"},
|
||||
{"tai", "太台抬态泰苔"},
|
||||
{"tan", "谈弹叹探贪摊滩瘫"},
|
||||
{"tang", "堂糖汤唐躺烫塘"},
|
||||
{"tao", "讨套逃掏桃掏"},
|
||||
{"te", "特"},
|
||||
{"teng", "疼腾藤"},
|
||||
{"ti", "体题提替踢蹄剃"},
|
||||
{"tian", "天田甜填添"},
|
||||
{"tiao", "条跳挑调"},
|
||||
{"tie", "铁贴帖"},
|
||||
{"ting", "听停厅挺庭亭艇"},
|
||||
{"tong", "同通痛统童筒铜桶"},
|
||||
{"tou", "头投偷透"},
|
||||
{"tu", "图土突途徒吐兔涂"},
|
||||
{"tuan", "团"},
|
||||
{"tui", "推退腿褪"},
|
||||
{"tun", "吞屯臀"},
|
||||
{"tuo", "托脱拖妥驼椭"},
|
||||
{"wa", "挖瓦蛙娃袜"},
|
||||
{"wai", "外歪"},
|
||||
{"wan", "完万晚玩弯碗湾挽"},
|
||||
{"wang", "王往望网忘旺亡"},
|
||||
{"wei", "为位未卫维微围委谓违唯尾"},
|
||||
{"wen", "问文闻温稳吻纹"},
|
||||
{"weng", "翁嗡瓮"},
|
||||
{"wo", "我握窝卧沃"},
|
||||
{"wu", "无五物误务武舞午污伍"},
|
||||
{"xi", "系西习戏喜细吸希洗悉溪膝席惜"},
|
||||
{"xia", "下夏吓虾峡狭暇霞"},
|
||||
{"xian", "先现线县显险鲜闲限陷献"},
|
||||
{"xiang", "想向相像响项香乡箱详"},
|
||||
{"xiao", "小笑校晓消销肖效萧"},
|
||||
{"xie", "写些鞋谢协血携斜泄"},
|
||||
{"xin", "新心信辛欣薪芯"},
|
||||
{"xing", "行性形星兴型醒幸姓"},
|
||||
{"xiong", "雄胸凶兄熊"},
|
||||
{"xiu", "修秀休羞锈袖嗅"},
|
||||
{"xu", "需须许序续徐虚绪蓄"},
|
||||
{"xuan", "选宣旋悬玄轩"},
|
||||
{"xue", "学雪血穴靴"},
|
||||
{"xun", "寻讯训迅询巡循旬"},
|
||||
{"ya", "压呀牙亚讶雅鸦鸭"},
|
||||
{"yan", "眼言演烟严沿研盐验颜宴艳延"},
|
||||
{"yang", "样养洋阳羊央仰扬"},
|
||||
{"yao", "要药摇腰咬耀邀遥谣"},
|
||||
{"ye", "也业夜叶爷野冶页"},
|
||||
{"yi", "一已以意义医衣依易忆疑谊益议"},
|
||||
{"yin", "因音引银印隐饮阴姻"},
|
||||
{"ying", "应英影营迎硬赢映鹰"},
|
||||
{"yo", "哟"},
|
||||
{"yong", "用永拥勇涌泳庸"},
|
||||
{"you", "有又由右油游友优幼忧尤呦"},
|
||||
{"yu", "于与语雨鱼玉育余遇欲愈预域娱"},
|
||||
{"yuan", "元原员院远愿圆园源冤缘"},
|
||||
{"yue", "月越约阅跃悦岳"},
|
||||
{"yun", "云运允匀韵孕"},
|
||||
{"za", "杂砸咋"},
|
||||
{"zai", "在再载咱灾宰"},
|
||||
{"zan", "咱赞暂攒"},
|
||||
{"zang", "藏脏葬"},
|
||||
{"zao", "早造遭糟澡灶燥躁"},
|
||||
{"ze", "则责择泽"},
|
||||
{"zei", "贼"},
|
||||
{"zen", "怎"},
|
||||
{"zeng", "增赠曾憎"},
|
||||
{"zha", "炸查扎眨乍榨渣"},
|
||||
{"zhai", "宅摘窄债斋"},
|
||||
{"zhan", "战站占展沾粘斩"},
|
||||
{"zhang", "张章长掌涨帐障丈"},
|
||||
{"zhao", "找照招着朝召兆"},
|
||||
{"zhe", "这者着折哲浙"},
|
||||
{"zhen", "真阵镇针震振诊枕"},
|
||||
{"zheng", "正整证政争征挣睁郑"},
|
||||
{"zhi", "只之至制知指直纸志治支止职织质执"},
|
||||
{"zhong", "中种重众忠终钟仲"},
|
||||
{"zhou", "周州洲舟昼皱骤轴"},
|
||||
{"zhu", "主住注助著朱猪竹煮筑祝驻"},
|
||||
{"zhua", "抓"},
|
||||
{"zhuai", "拽"},
|
||||
{"zhuan", "转专赚传砖"},
|
||||
{"zhuang", "装状撞庄壮"},
|
||||
{"zhui", "追坠缀"},
|
||||
{"zhun", "准"},
|
||||
{"zhuo", "桌捉着灼卓浊"},
|
||||
{"zi", "子自字资紫仔兹姿咨"},
|
||||
{"zong", "总宗纵综棕踪"},
|
||||
{"zou", "走奏揍邹"},
|
||||
{"zu", "组足祖族租阻卒"},
|
||||
{"zuan", "钻攥"},
|
||||
{"zui", "最嘴罪醉"},
|
||||
{"zun", "尊遵"},
|
||||
{"zuo", "做作坐座左昨"},
|
||||
};
|
||||
|
||||
#define WORD_N 329
|
||||
static const py_word WORDS[WORD_N] = {
|
||||
{"nihao","你好",10000},
|
||||
{"zhongguo","中国",9200},
|
||||
{"women","我们",9000},
|
||||
{"shijie","世界",8600},
|
||||
{"shijian","时间",8400},
|
||||
{"xianzai","现在",8300},
|
||||
{"keyi","可以",8200},
|
||||
{"meiyou","没有",8100},
|
||||
{"yijing","已经",8000},
|
||||
{"haode","好的",7900},
|
||||
{"pengyou","朋友",7800},
|
||||
{"gongzuo","工作",7700},
|
||||
{"shenghuo","生活",7600},
|
||||
{"xuexi","学习",7500},
|
||||
{"xihuan","喜欢",7400},
|
||||
{"beijing","北京",7300},
|
||||
{"shanghai","上海",7300},
|
||||
{"yinyue","音乐",7200},
|
||||
{"sousuo","搜索",7100},
|
||||
{"xiaza","下载",7000},
|
||||
{"wenjian","文件",7000},
|
||||
{"wenjianjia","文件夹",6900},
|
||||
{"shezhi","设置",6900},
|
||||
{"yingyong","应用",6900},
|
||||
{"tongzhi","通知",6800},
|
||||
{"zhongwen","中文",6800},
|
||||
{"pinyin","拼音",6800},
|
||||
{"meiti","媒体",6700},
|
||||
{"bofang","播放",6700},
|
||||
{"zanting","暂停",6600},
|
||||
{"jixu","继续",6600},
|
||||
{"shangyi","上一",6600},
|
||||
{"xiayi","下一",6600},
|
||||
{"liebiao","列表",6550},
|
||||
{"geshou","歌手",6500},
|
||||
{"gequ","歌曲",6500},
|
||||
{"zhuanji","专辑",6450},
|
||||
{"gouwu","购物",6400},
|
||||
{"dianhua","电话",6400},
|
||||
{"shouji","手机",6400},
|
||||
{"diannao","电脑",6350},
|
||||
{"dianying","电影",6300},
|
||||
{"dianshi","电视",6300},
|
||||
{"tushu","图书",6250},
|
||||
{"xuexiao","学校",6200},
|
||||
{"daxue","大学",6200},
|
||||
{"xiaoxue","小学",6150},
|
||||
{"gongsi","公司",6100},
|
||||
{"laoshi","老师",6100},
|
||||
{"tongxue","同学",6050},
|
||||
{"jiating","家庭",6000},
|
||||
{"baba","爸爸",6000},
|
||||
{"mama","妈妈",6000},
|
||||
{"gege","哥哥",5900},
|
||||
{"jiejie","姐姐",5900},
|
||||
{"didi","弟弟",5850},
|
||||
{"meimei","妹妹",5850},
|
||||
{"pengyoumen","朋友们",5800},
|
||||
{"nihaoma","你好吗",7000},
|
||||
{"zhongguoren","中国人",6200},
|
||||
{"shanghairen","上海人",4200},
|
||||
{"beijingren","北京人",4200},
|
||||
{"shenme","什么",6600},
|
||||
{"weishenme","为什么",6500},
|
||||
{"zemme","怎么",6500},
|
||||
{"zenmeyang","怎么样",6400},
|
||||
{"shenmeshihou","什么时候",6300},
|
||||
{"nar","哪儿",6200},
|
||||
{"duoshao","多少",6400},
|
||||
{"dui","对",8200},
|
||||
{"bushi","不是",8100},
|
||||
{"shi","是",8300},
|
||||
{"yexu","也许",6300},
|
||||
{"keshi","可是",6200},
|
||||
{"suoyi","所以",6300},
|
||||
{"ranhou","然后",6200},
|
||||
{"danshi","但是",6200},
|
||||
{"haishi","还是",6400},
|
||||
{"huozhe","或者",6300},
|
||||
{"bingqie","并且",6200},
|
||||
{"erqie","而且",6300},
|
||||
{"yinwei","因为",6500},
|
||||
{"suoyi","所以",6300},
|
||||
{"jintian","今天",6500},
|
||||
{"mingtian","明天",6400},
|
||||
{"zuotian","昨天",6300},
|
||||
{"xingqi","星期",6200},
|
||||
{"zhoumo","周末",6100},
|
||||
{"shangwu","上午",6000},
|
||||
{"xiawu","下午",6000},
|
||||
{"wanshang","晚上",6100},
|
||||
{"zaoshang","早上",6000},
|
||||
{"zhongwu","中午",5900},
|
||||
{"zhidao","知道",6800},
|
||||
{"mingbai","明白",6300},
|
||||
{"renshi","认识",6300},
|
||||
{"liaojie","了解",6200},
|
||||
{"xiangxin","相信",6200},
|
||||
{"xiwang","希望",6300},
|
||||
{"juede","觉得",6500},
|
||||
{"xianzai","现在",8300},
|
||||
{"zhende","真的",6400},
|
||||
{"feichang","非常",6400},
|
||||
{"tai","太",7800},
|
||||
{"hen","很",8000},
|
||||
{"dou","都",7900},
|
||||
{"ye","也",7900},
|
||||
{"zai","在",8300},
|
||||
{"you","有",8300},
|
||||
{"ba","吧",7800},
|
||||
{"ma","吗",7900},
|
||||
{"ne","呢",7800},
|
||||
{"a","啊",7900},
|
||||
{"jianghu","江湖",5400},
|
||||
{"shijie","世界",8600},
|
||||
{"xianshi","显示",6400},
|
||||
{"shebei","设备",6500},
|
||||
{"kongzhi","控制",6500},
|
||||
{"yuancheng","远程",6300},
|
||||
{"zhijie","直接",6400},
|
||||
{"fuwu","服务",6600},
|
||||
{"qidong","启动",6400},
|
||||
{"guanbi","关闭",6300},
|
||||
{"chongqi","重启",6200},
|
||||
{"gengxin","更新",6400},
|
||||
{"anzhuang","安装",6300},
|
||||
{"xiezai","卸载",6100},
|
||||
{"denglu","登录",6200},
|
||||
{"zhuce","注册",6100},
|
||||
{"shenfen","身份",6000},
|
||||
{"mima","密码",6100},
|
||||
{"zhanghu","账户",6000},
|
||||
{"anquan","安全",6200},
|
||||
{"shebei","设备",6500},
|
||||
{"wangluo","网络",6600},
|
||||
{"lianjie","连接",6500},
|
||||
{"duankai","断开",6100},
|
||||
{"wuxian","无线",6200},
|
||||
{"xinyongka","信用卡",5800},
|
||||
{"weixin","微信",6000},
|
||||
{"zhifu","支付",6200},
|
||||
{"shoukuan","收款",5900},
|
||||
{"gongxiang","共享",6000},
|
||||
{"wenjian","文件",7000},
|
||||
{"mulu","目录",6200},
|
||||
{"lujing","路径",6100},
|
||||
{"mingzi","名字",6200},
|
||||
{"mingcheng","名称",6400},
|
||||
{"liulan","浏览",6100},
|
||||
{"dakai","打开",6300},
|
||||
{"baocun","保存",6300},
|
||||
{"shanchua","删除",6400},
|
||||
{"fuzhi","复制",6200},
|
||||
{"jiantie","粘贴",6100},
|
||||
{"yidong","移动",6200},
|
||||
{"chongming","重命名",6100},
|
||||
{"chuanjian","创建",6200},
|
||||
{"xinjian","新建",6200},
|
||||
{"daoru","导入",6000},
|
||||
{"daochu","导出",6000},
|
||||
{"yinyue","音乐",7200},
|
||||
{"geci","歌词",6200},
|
||||
{"geshou","歌手",6500},
|
||||
{"gequ","歌曲",6500},
|
||||
{"xunhuan","循环",6100},
|
||||
{"shunxu","顺序",6100},
|
||||
{"suiji","随机",6000},
|
||||
{"danqu","单曲",6000},
|
||||
{"quanshen","全申",5600},
|
||||
{"anli","案例",5600},
|
||||
{"zhangdan","账单",5600},
|
||||
{"chongzhi","充值",5800},
|
||||
{"jifen","积分",5800},
|
||||
{"huiyuan","会员",6000},
|
||||
{"shangcheng","商城",5900},
|
||||
{"dingdan","订单",6000},
|
||||
{"wuliu","物流",5800},
|
||||
{"kuaidi","快递",5800},
|
||||
{"didai","地带",5600},
|
||||
{"quyu","区域",6100},
|
||||
{"shuju","数据",6700},
|
||||
{"wenben","文本",6300},
|
||||
{"shuru","输入",6400},
|
||||
{"shuchu","输出",6300},
|
||||
{"jiance","检测",6200},
|
||||
{"yunchu","运出",5600},
|
||||
{"ceshi","测试",6300},
|
||||
{"fangshi","方式",6400},
|
||||
{"mubiao","目标",6200},
|
||||
{"zhuangtai","状态",6500},
|
||||
{"jieduan","阶段",6100},
|
||||
{"banben","版本",6200},
|
||||
{"gongneng","功能",6600},
|
||||
{"canshu","参数",6100},
|
||||
{"jieguo","结果",6500},
|
||||
{"cuowu","错误",6300},
|
||||
{"tishi","提示",6300},
|
||||
{"warn","警告",5600},
|
||||
{"xinxi","信息",6700},
|
||||
{"shuju","数据",6700},
|
||||
{"anquan","安全",6200},
|
||||
{"qiye","企业",6100},
|
||||
{"yonghu","用户",6600},
|
||||
{"touxiang","头像",6000},
|
||||
{"jinqing","尽情",5800},
|
||||
{"zaixian","在线",6200},
|
||||
{"xiaoxi","消息",6400},
|
||||
{"pengyou","朋友",7800},
|
||||
{"guanxi","关系",6200},
|
||||
{"meili","美丽",6100},
|
||||
{"piaoliang","漂亮",6100},
|
||||
{"kuaile","快乐",6200},
|
||||
{"kaixin","开心",6200},
|
||||
{"xingfu","幸福",6100},
|
||||
{"jiankang","健康",6200},
|
||||
{"shenti","身体",6200},
|
||||
{"anquan","安全",6200},
|
||||
{"baozhang","保障",5900},
|
||||
{"huanjing","环境",6200},
|
||||
{"ziyuan","资源",6200},
|
||||
{"nengyuan","能源",6000},
|
||||
{"huanbao","环保",5900},
|
||||
{"zhineng","智能",6200},
|
||||
{"renlei","人类",6100},
|
||||
{"shehui","社会",6200},
|
||||
{"guojia","国家",6200},
|
||||
{"renmin","人民",6100},
|
||||
{"zhengfu","政府",6100},
|
||||
{"jingji","经济",6200},
|
||||
{"wenhua","文化",6200},
|
||||
{"jiaoyu","教育",6200},
|
||||
{"kexue","科学",6200},
|
||||
{"jishu","技术",6500},
|
||||
{"gongye","工业",6100},
|
||||
{"nongye","农业",5900},
|
||||
{"shangye","商业",6200},
|
||||
{"jinrong","金融",6100},
|
||||
{"yiyao","医药",5900},
|
||||
{"fangchan","房产",5900},
|
||||
{"qiche","汽车",6200},
|
||||
{"jixie","机械",6000},
|
||||
{"dianqi","电气",6000},
|
||||
{"ruan","软",6000},
|
||||
{"yingjian","硬件",6100},
|
||||
{"shujuku","数据库",6100},
|
||||
{"caozuo","操作",6400},
|
||||
{"xitong","系统",6600},
|
||||
{"pingtai","平台",6300},
|
||||
{"kuangjia","框架",6100},
|
||||
{"jiekou","接口",6200},
|
||||
{"yemian","页面",6300},
|
||||
{"chuangkou","窗口",6200},
|
||||
{"gundong","滚动",6100},
|
||||
{"shuaxin","刷新",6200},
|
||||
{"jiazai","加载",6300},
|
||||
{"huanchong","缓冲",6000},
|
||||
{"duilie","队列",5900},
|
||||
{"xiancheng","线程",6000},
|
||||
{"jincheng","进程",6100},
|
||||
{"neicun","内存",6200},
|
||||
{"yingpan","硬盘",6100},
|
||||
{"cunchu","存储",6100},
|
||||
{"wenjian","文件",7000},
|
||||
{"wenjianjia","文件夹",6900},
|
||||
{"tu","图",6400},
|
||||
{"tupian","图片",6400},
|
||||
{"zhaopian","照片",6200},
|
||||
{"shouji","手机",6400},
|
||||
{"xiangji","相机",6200},
|
||||
{"luoyin","录音",6100},
|
||||
{"shipin","视频",6400},
|
||||
{"shexiang","摄像",6000},
|
||||
{"yinyue","音乐",7200},
|
||||
{"bojing","剧情",5800},
|
||||
{"juchang","剧场",5800},
|
||||
{"dianying","电影",6300},
|
||||
{"dianshiju","电视剧",6200},
|
||||
{"jilu","记录",6200},
|
||||
{"zuji","足迹",5600},
|
||||
{"ditu","地图",6200},
|
||||
{"daohang","导航",6100},
|
||||
{"weizhi","位置",6200},
|
||||
{"dingwei","定位",6100},
|
||||
{"zhuanti","专题",5900},
|
||||
{"fenlei","分类",6200},
|
||||
{"biaqian","标签",6100},
|
||||
{"zhuxiao","注销",5800},
|
||||
{"zhuxiao","注销",5800},
|
||||
{"huanying","欢迎",6200},
|
||||
{"liuyan","留言",6000},
|
||||
{"pinglun","评论",6200},
|
||||
{"dianzan","点赞",6100},
|
||||
{"fenxiang","分享",6300},
|
||||
{"shoucang","收藏",6300},
|
||||
{"guanzhu","关注",6200},
|
||||
{"yinshua","印刷",5900},
|
||||
{"banshi","办事",5700},
|
||||
{"shenpi","审批",5800},
|
||||
{"jinzhi","禁止",6000},
|
||||
{"yunxu","允许",6100},
|
||||
{"queren","确认",6300},
|
||||
{"queding","确定",6300},
|
||||
{"qian","前",7600},
|
||||
{"hou","后",7600},
|
||||
{"zuo","左",7500},
|
||||
{"you","右",7500},
|
||||
{"shang","上",8000},
|
||||
{"xia","下",8000},
|
||||
{"zhongjian","中间",6100},
|
||||
{"pangbian","旁边",6000},
|
||||
{"limian","里面",6100},
|
||||
{"waimian","外面",6000},
|
||||
{"qianmian","前面",6100},
|
||||
{"houmian","后面",6100},
|
||||
{"shangmian","上面",6200},
|
||||
{"xiamian","下面",6100},
|
||||
{"dibiao","地表",5600},
|
||||
{"gongyuan","公园",6000},
|
||||
{"jie","街",6000},
|
||||
{"lukou","路口",5900},
|
||||
{"fangxiang","方向",6200},
|
||||
{"dongnan","东南",5800},
|
||||
{"xinxi","信息",6700},
|
||||
{"shuzime","数字",6400},
|
||||
{"zimu","字母",6100},
|
||||
{"hanzi","汉字",6300},
|
||||
{"wenhao","问号",5900},
|
||||
{"juhao","句号",5900},
|
||||
{"douhao","逗号",5900},
|
||||
};
|
||||
|
||||
#endif /* MEOW_PINYIN_LEX_H */
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* meow_ui.h — MeowOS 应用框架 SDK 公共 API(B1)。
|
||||
*
|
||||
* 蓝本:MEOW_APP_SDK.md §3-§8。应用只调本头声明的 API;绝不 open/mmap/ioctl
|
||||
* /dev/fb0、绝不 open /dev/input/event* —— 屏幕与输入由 meow_ui 接管。
|
||||
*
|
||||
* 约定:
|
||||
* - 坐标 0,0 = 左上;所有颜色为 24-bit RGB(0xRRGGBB),内部转 RGB565。
|
||||
* - 所有绘制先进离屏 back_buf,调用 meow_ui_flush()/flush_region() 才上屏。
|
||||
* - 文本字号:28(标题/状态栏)、20(正文/按钮)。
|
||||
*
|
||||
* 与设计文档的偏差(实现期定案,见 MEOW_APP_SDK.md §12 更新):
|
||||
* [D1] meow_ui_opts 由实现补充(宽/高覆写,0=自动读 /dev/fb0);文档只引用未定义。
|
||||
* [D2] meow_list_create 的数据源回调签名由实现定案(文档留占位注释)。
|
||||
* [D3] meow_img_blend 内部实现补充(见 meow_ui_internal.h),非公共 API 增量。
|
||||
* [D4] 增加 meow_theme_defaults()/meow_theme_current() 便于脱机(无 settings 文件)使用。
|
||||
*
|
||||
* ------------------------------------------------------------------
|
||||
* 公共头模块索引(按模块拆分,本头为**伞头**:只做 #include 聚合,不定义新 API):
|
||||
*
|
||||
* include/meow_ui_types.h 核心类型/常量(物理键 keycode、滑动方向、meow_widget/meow_img 句柄)
|
||||
* include/meow_ui_draw.h 绘制层(§3)+ 时间(§7)+ 刷新率调度(§7.1)
|
||||
* include/meow_ui_image.h 图片层(§4)
|
||||
* include/meow_ui_theme.h 主题/配置(§8:meow_theme、MD3 色板、风格族)
|
||||
* include/meow_ui_ime.h 输入法(§5.1:ASCII/NUM/PINYIN 软键盘)
|
||||
* include/meow_ui_widgets.h 控件层(§5 + §5.8 基座 + §5.8.6 MD3 组件 + §5.5 W_LISTVIEW)
|
||||
* include/meow_ui_input.h 输入层/生命周期(§6)+ 通用长按(§6.3)
|
||||
* include/meow_ui_activity.h Activity 栈 + 三层控件模型(§6.1)
|
||||
* include/meow_ui_zorder.h 绘制物注册表 / z 序 / 区域重画(§6.2)
|
||||
* include/meow_ui_statusbar.h 全局状态栏 / 全屏(§3.1)
|
||||
* include/meow_ui_dialog.h 模态 Dialog / BottomSheet(§5.8.6)
|
||||
* 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_audio.h 独占音频客户端(§11)
|
||||
*
|
||||
* 其余模块头(非伞头、App 按需显式 include,均保持不动):
|
||||
* meow_ui_view.h / meow_ui_nav.h / meow_ui_media.h / meow_ui_sys.h / meow_ui_vol.h
|
||||
* ------------------------------------------------------------------
|
||||
*
|
||||
* 拆分不改变任何 API 名称/类型/宏(-Wall -Wextra 0 warning,既有 App 零改动)。
|
||||
*/
|
||||
#ifndef MEOW_UI_H
|
||||
#define MEOW_UI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "include/meow_ui_types.h"
|
||||
#include "include/meow_ui_draw.h"
|
||||
#include "include/meow_ui_image.h"
|
||||
#include "include/meow_ui_ime.h"
|
||||
#include "include/meow_ui_input.h"
|
||||
#include "include/meow_ui_activity.h"
|
||||
#include "include/meow_ui_zorder.h"
|
||||
#include "include/meow_ui_statusbar.h"
|
||||
#include "include/meow_ui_theme.h"
|
||||
#include "include/meow_ui_widgets.h"
|
||||
#include "include/meow_ui_dialog.h"
|
||||
#include "include/meow_ui_log.h"
|
||||
#include "include/meow_ui_notify.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
#include "include/meow_ui_font.h"
|
||||
#include "include/meow_ui_audio.h"
|
||||
|
||||
#endif /* MEOW_UI_H */
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* meow_ui_activity.c — Activity 栈(§6.1 ①)。
|
||||
*
|
||||
* 控件严格归属创建它的 Activity;命中/实时按压只在「当前 active Activity」的控件集里
|
||||
* 生效(见 meow_ui_widget.c 的 owner 绑定 + live_btn_hit/meow_widget_hit 过滤)。
|
||||
* 栈空时 active() 返回 0(根/default Activity)。
|
||||
*
|
||||
* 依赖:无(纯静态数组 + 无外部符号),宿主/真机均可链入,便于单测。
|
||||
* 语义:push = 压栈(新 Activity 成为 active); pop = 弹栈(回上级,栈空则停在根 0)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
#define MEOW_ACT_MAX 32
|
||||
|
||||
static int g_act[MEOW_ACT_MAX];
|
||||
static int g_act_n = 0;
|
||||
|
||||
void meow_ui_activity_push(int id)
|
||||
{
|
||||
if (g_act_n >= MEOW_ACT_MAX)
|
||||
return; /* 栈满:静默钳制(骨架级,不崩) */
|
||||
g_act[g_act_n++] = id;
|
||||
}
|
||||
|
||||
void meow_ui_activity_pop(void)
|
||||
{
|
||||
if (g_act_n > 0) g_act_n--;
|
||||
}
|
||||
|
||||
int meow_ui_activity_active(void)
|
||||
{
|
||||
return g_act_n > 0 ? g_act[g_act_n - 1] : 0;
|
||||
}
|
||||
|
||||
int meow_ui_activity_depth(void)
|
||||
{
|
||||
return g_act_n;
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
/* meow_ui_audio.c — App 侧独占音频客户端(SDK 公共件;见 meow_ui.h §11)。
|
||||
*
|
||||
* 定位:App 只经本 API 喂 PCM 给 meow_audiod(独占 hw:0,0 守护)。audiod 不在一律失败
|
||||
* (open 返回 MEOW_ERR_NOENT 等负错误码),不做隐式直开回退;调试可 env MEOW_AUDIO_DIRECT=1 走自带
|
||||
* 直接 ALSA 直开(非正式能力)。
|
||||
*
|
||||
* 传输:AF_UNIX SOCK_STREAM 到 audiod socket(缺省 /usr/data/meow_audio.sock;env
|
||||
* MEOW_AUDIO_SOCK 覆盖,宿主/qemu)。帧格式复用 meow_ipc.h 16B 头 + m_audio_* 负载(同一份)。
|
||||
*
|
||||
* 线程模型(懒连 + 内部读线程):
|
||||
* - 首次 open/query 时 connect → 懒启动读线程(poll 读 audiod 下发帧)。
|
||||
* - 读线程为唯一读方:PREEMPTED → 调 on_preempt 回调 + 标写死;OPEN_ACK/STATE/ACK(匹配
|
||||
* 当前挂起请求)→ 拷贝到 g_resp、唤醒调用方(条件变量)。
|
||||
* - 写路径(write/pause/resume/close)由 App 线程在另一方向写同一 fd(全双工,互不干扰)。
|
||||
*
|
||||
* 格式归一:open 返回前记录 grant_rate/grant_ch/grant_fmt;meow_audio_write 把 native(open 参数)
|
||||
* 样本逐帧转成 grant 格式(mono→stereo 复制 / S16↔S32 定值升降位),分批 ≤4096B 发 MSG_AUDIO_WRITE。
|
||||
* 无 SRC:grant_rate == native rate(audiod 把 DAC 原生率设为流率)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sound/asound.h>
|
||||
#include <sys/ioctl.h>
|
||||
#define UI_AUDIO_HAVE_ALSA 1
|
||||
#endif
|
||||
|
||||
#define AUD_SOCK_DEFAULT "/usr/data/meow_audio.sock"
|
||||
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
|
||||
static int g_fd = -1;
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on = 0;
|
||||
static volatile int g_quit = 0;
|
||||
|
||||
/* 当前流状态 */
|
||||
static int g_has_stream = 0;
|
||||
static uint32_t g_sid = 0;
|
||||
static int g_dead = 0; /* 收到 PREEMPTED 后置 1:write 返回负值 */
|
||||
static int g_own_rate = 0, g_own_ch = 0, g_own_fmt = 0; /* native(open 参数) */
|
||||
static int g_grant_rate = 0, g_grant_ch = 0, g_grant_fmt = 0;
|
||||
|
||||
static meow_audio_preempt_fn g_preempt_cb = NULL;
|
||||
static void *g_preempt_ctx = NULL;
|
||||
|
||||
/* 挂起请求(读线程分发;一次只有一个) */
|
||||
static uint32_t g_pending = 0; /* req_id,0=无挂起 */
|
||||
static uint16_t g_resp_type = 0;
|
||||
static uint8_t g_resp[MEOW_IPC_MAX];
|
||||
static uint32_t g_resp_len = 0;
|
||||
|
||||
/* 调试直开(env MEOW_AUDIO_DIRECT=1) */
|
||||
static int g_direct = 0;
|
||||
static int g_dir_fd = -1;
|
||||
|
||||
static const char *sock_path(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_AUDIO_SOCK");
|
||||
return (e && e[0]) ? e : AUD_SOCK_DEFAULT;
|
||||
}
|
||||
|
||||
/* 连接 audiod。**全部有界**:connect 非阻塞 + poll 2s(对端 listen backlog 满/wedge 时
|
||||
* 阻塞 connect 会把 App 播放线程永久压进内核——真机长流卡死时 ncm 卡死的同款路径);
|
||||
* 建连后 SO_SNDTIMEO/RCVTIMEO 2s:audiod 停摆时 write 超时返回错误 → 标记流死,
|
||||
* App 走「重新 open」恢复路径,绝不永久阻塞。 */
|
||||
#define AUD_CONN_TMO_MS 2000
|
||||
static int connect_path(const char *path)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, path, sizeof a.sun_path - 1);
|
||||
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
|
||||
if (r < 0 && errno == EINPROGRESS) {
|
||||
struct pollfd p;
|
||||
p.fd = fd; p.events = POLLOUT; p.revents = 0;
|
||||
if (poll(&p, 1, AUD_CONN_TMO_MS) > 0) {
|
||||
int err = 0;
|
||||
socklen_t el = sizeof err;
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
|
||||
} else {
|
||||
r = -1; /* 超时/错误 */
|
||||
}
|
||||
}
|
||||
if (r < 0) { close(fd); return -1; }
|
||||
if (fl >= 0) fcntl(fd, F_SETFL, fl); /* 回阻塞模式;读写超时由 SO_*TIMEO 兜底 */
|
||||
struct timeval tv;
|
||||
tv.tv_sec = AUD_CONN_TMO_MS / 1000; tv.tv_usec = 0;
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
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 -1; }
|
||||
if (w == 0) return -1;
|
||||
off += (size_t)w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
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 -1; }
|
||||
if (r == 0) return -1;
|
||||
off += (size_t)r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
uint16_t ver = MEOW_IPC_VERSION;
|
||||
memcpy(hdr + 0, &magic, 4);
|
||||
memcpy(hdr + 4, &ver, 2);
|
||||
memcpy(hdr + 6, &type, 2);
|
||||
memcpy(hdr + 8, &req_id, 4);
|
||||
memcpy(hdr + 12, &len, 4);
|
||||
if (write_all(fd, hdr, sizeof hdr) != 0) return -1;
|
||||
if (len && payload && write_all(fd, payload, len) != 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
if (read_all(fd, hdr, sizeof hdr) != 0) return -1;
|
||||
uint32_t magic, len; uint16_t ver;
|
||||
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2);
|
||||
memcpy(&len, hdr + 12, 4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||||
if (len > rcap) len = rcap;
|
||||
if (len > 0 && read_all(fd, rbuf, len) != 0) return -1;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
static void *audio_thread_main(void *arg);
|
||||
|
||||
/* (调用方已持 mu)标记断线并关闭。 */
|
||||
static void mark_dead_locked(void)
|
||||
{
|
||||
if (g_fd >= 0) { close(g_fd); g_fd = -1; }
|
||||
g_dead = 1;
|
||||
}
|
||||
|
||||
/* (调用方已持 mu)懒连接 + 懒起读线程。成功 0;audiod 不在 -1。 */
|
||||
static int conn_locked(void)
|
||||
{
|
||||
if (g_fd >= 0) return g_fd;
|
||||
if (g_direct) return -2; /* 调试直开:不连 socket */
|
||||
const char *sp = sock_path();
|
||||
int fd = connect_path(sp);
|
||||
if (fd < 0) return -1;
|
||||
int one = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof one);
|
||||
g_fd = fd;
|
||||
if (!g_thr_on) { g_quit = 0; g_thr_on = 1; pthread_create(&g_thr, NULL, audio_thread_main, NULL); pthread_detach(g_thr); }
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* ---- 调试直开(MEOW_AUDIO_DIRECT=1,仅开发) ---- */
|
||||
#ifdef UI_AUDIO_HAVE_ALSA
|
||||
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
|
||||
static void pivset(struct snd_interval *iv, unsigned val)
|
||||
{
|
||||
iv->min = iv->max = val; iv->integer = 1; iv->openmin = iv->openmax = 0; iv->empty = 0;
|
||||
}
|
||||
static int direct_alsa_open(int rate)
|
||||
{
|
||||
int fd = open("/dev/snd/pcmC0D0p", O_RDWR);
|
||||
if (fd < 0) return -1;
|
||||
struct snd_pcm_hw_params p;
|
||||
/* 「全放行」初始化(snd_pcm_hw_params_any 语义):memset 0 的 interval 会被内核当成
|
||||
* 「只能取 0」约束 → refine 必空 → HW_PARAMS 恒 EINVAL(audiod 同款坑,勿回退)。 */
|
||||
memset(&p, 0, sizeof p);
|
||||
unsigned nm = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1;
|
||||
for (unsigned i = 0; i < nm; i++)
|
||||
for (unsigned b = 0; b < sizeof p.masks[i].bits / sizeof p.masks[i].bits[0]; b++)
|
||||
p.masks[i].bits[b] = ~0u;
|
||||
unsigned ni = SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1;
|
||||
for (unsigned i = 0; i < ni; i++) { p.intervals[i].min = 0; p.intervals[i].max = ~0u; }
|
||||
p.flags = 0;
|
||||
/* mask 先清零再单点(pmask 只 OR):全 1 的 access 会被内核 choose 成 MMAP_INTERLEAVED
|
||||
* → write() EINVAL(audiod 同款第二坑,勿回退)。 */
|
||||
struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(am, 0, sizeof *am); pmask(am, SNDRV_PCM_ACCESS_RW_INTERLEAVED);
|
||||
struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(fm, 0, sizeof *fm); pmask(fm, SNDRV_PCM_FORMAT_S16_LE);
|
||||
struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD);
|
||||
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], 2);
|
||||
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL], (unsigned)rate);
|
||||
p.rmask = ~0u; p.info = 0; p.msbits = 0; p.rate_num = 0; p.rate_den = 0;
|
||||
if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p) != 0) { close(fd); return -1; }
|
||||
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0);
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ---- 格式转换 ---- */
|
||||
static int fmt_bps(int fmt)
|
||||
{
|
||||
switch (fmt) {
|
||||
case AUDIO_FMT_S16: return 2;
|
||||
case AUDIO_FMT_S24:
|
||||
case AUDIO_FMT_S32:
|
||||
case AUDIO_FMT_FLOAT: return 4;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
static int32_t sample_from_native(const uint8_t *p, int fmt)
|
||||
{
|
||||
if (fmt == AUDIO_FMT_S16) { int16_t v; memcpy(&v, p, 2); return v; }
|
||||
int32_t v; memcpy(&v, p, 4); return v; /* S32/S24(32-in-32)/FLOAT 按 32 位采样 */
|
||||
}
|
||||
static void sample_to_grant(uint8_t *p, int fmt, int32_t v)
|
||||
{
|
||||
if (fmt == AUDIO_FMT_S16) { int16_t x = (int16_t)v; memcpy(p, &x, 2); return; }
|
||||
memcpy(p, &v, 4);
|
||||
}
|
||||
static int32_t convert_sample(int32_t v, int nfmt, int gfmt)
|
||||
{
|
||||
int nb = (nfmt == AUDIO_FMT_S16) ? 16 : 32;
|
||||
int gb = (gfmt == AUDIO_FMT_S16) ? 16 : 32;
|
||||
if (gb > nb) return v << (gb - nb);
|
||||
if (gb < nb) return v >> (nb - gb);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* 发一批 WRITE(≤MEOW_IPC_MAX,帧对齐;调用方已持 mu) */
|
||||
static int send_write_chunk(const uint8_t *p, uint32_t len)
|
||||
{
|
||||
if (g_fd < 0) { g_dead = 1; return -1; }
|
||||
if (send_frame(g_fd, MSG_AUDIO_WRITE, 0, p, len) != 0) { mark_dead_locked(); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- 公开 API ---- */
|
||||
|
||||
int meow_audio_open(int rate, int ch, int fmt)
|
||||
{
|
||||
/* env 检查:调试直开 */
|
||||
const char *de = getenv("MEOW_AUDIO_DIRECT");
|
||||
g_direct = (de && de[0] == '1') ? 1 : 0;
|
||||
|
||||
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) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
g_own_rate = rate; g_own_ch = ch; g_own_fmt = fmt;
|
||||
|
||||
if (g_direct) {
|
||||
#ifdef UI_AUDIO_HAVE_ALSA
|
||||
int fd = direct_alsa_open(rate);
|
||||
if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NODEV; }
|
||||
g_dir_fd = fd;
|
||||
g_grant_rate = rate; g_grant_ch = 2; g_grant_fmt = AUDIO_FMT_S16;
|
||||
g_sid = 1; g_has_stream = 1; g_dead = 0;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 1;
|
||||
#else
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return MEOW_ERR_NODEV;
|
||||
#endif
|
||||
}
|
||||
|
||||
int fd = conn_locked();
|
||||
if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; } /* audiod 不在 */
|
||||
|
||||
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;
|
||||
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1); /* 单调递增(单个挂起) */
|
||||
g_pending = req;
|
||||
g_resp_len = 0;
|
||||
if (send_frame(g_fd, MSG_AUDIO_OPEN, req, &o, sizeof o) != 0) { mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; }
|
||||
/* 等响应(3s 超时) */
|
||||
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
|
||||
while (g_pending == req) {
|
||||
int r = pthread_cond_timedwait(&g_cond, &g_mu, &ts);
|
||||
if (r != 0) { mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_TIMEOUT; }
|
||||
}
|
||||
if (g_resp_type != MSG_AUDIO_OPEN_ACK || g_resp_len < sizeof(m_audio_open_ack_t)) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return MEOW_ERR_IO;
|
||||
}
|
||||
m_audio_open_ack_t a;
|
||||
memcpy(&a, g_resp, sizeof a);
|
||||
if (a.err != MEOW_OK) { pthread_mutex_unlock(&g_mu); return (int)a.err; }
|
||||
g_sid = a.stream_id;
|
||||
g_grant_rate = (int)a.grant_rate; g_grant_ch = (int)a.grant_ch; g_grant_fmt = (int)a.grant_fmt;
|
||||
g_has_stream = 1; g_dead = 0;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return (int)g_sid;
|
||||
}
|
||||
|
||||
int meow_audio_write(const void *pcm, int frames)
|
||||
{
|
||||
if (!pcm || frames <= 0) return 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_direct) {
|
||||
size_t nb = (size_t)frames * (size_t)g_grant_ch * (size_t)fmt_bps(g_grant_fmt);
|
||||
const uint8_t *p = (const uint8_t *)pcm;
|
||||
size_t off = 0;
|
||||
while (off < nb) {
|
||||
ssize_t w = write(g_dir_fd, p + off, nb - off);
|
||||
if (w < 0) { if (errno == EINTR) continue; pthread_mutex_unlock(&g_mu); return -1; }
|
||||
off += (size_t)w;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return frames;
|
||||
}
|
||||
if (!g_has_stream || g_dead || g_fd < 0) { pthread_mutex_unlock(&g_mu); return -1; }
|
||||
/* 归一:native → grant */
|
||||
int ownfs = g_own_ch * fmt_bps(g_own_fmt);
|
||||
int gfs = g_grant_ch * fmt_bps(g_grant_fmt);
|
||||
static uint8_t out[MEOW_IPC_MAX];
|
||||
size_t ofs = 0;
|
||||
int done_frames = 0;
|
||||
const uint8_t *np = (const uint8_t *)pcm;
|
||||
for (int i = 0; i < frames; i++) {
|
||||
/* 取 native 帧样本 */
|
||||
int32_t s[2];
|
||||
for (int c = 0; c < g_own_ch && c < 2; c++)
|
||||
s[c] = sample_from_native(np + (size_t)i * ownfs + (size_t)c * fmt_bps(g_own_fmt), g_own_fmt);
|
||||
/* 整帧才能进 payload(每帧不得跨两个 WRITE 分片) */
|
||||
if (ofs + (size_t)gfs > sizeof out) {
|
||||
if (send_write_chunk(out, (uint32_t)ofs) != 0) { pthread_mutex_unlock(&g_mu); return -1; }
|
||||
ofs = 0;
|
||||
}
|
||||
/* 生成 grant 帧:own_ch==1 → 复制;own_ch==2 → 直通 */
|
||||
for (int c = 0; c < g_grant_ch && c < 2; c++) {
|
||||
int32_t v = (g_own_ch == 1) ? s[0] : s[c];
|
||||
v = convert_sample(v, g_own_fmt, g_grant_fmt);
|
||||
sample_to_grant(out + ofs, g_grant_fmt, v);
|
||||
ofs += (size_t)fmt_bps(g_grant_fmt);
|
||||
}
|
||||
done_frames++;
|
||||
}
|
||||
if (ofs && send_write_chunk(out, (uint32_t)ofs) != 0) { pthread_mutex_unlock(&g_mu); return -1; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return done_frames;
|
||||
}
|
||||
|
||||
int meow_audio_pause(int sid, int paused)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_direct) { pthread_mutex_unlock(&g_mu); return 0; }
|
||||
if (!g_has_stream || g_sid != (uint32_t)sid) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; }
|
||||
m_audio_stream_t s; s.stream_id = (uint32_t)sid;
|
||||
uint32_t req = g_pending + 1; g_pending = req;
|
||||
if (send_frame(g_fd, paused ? MSG_AUDIO_PAUSE : MSG_AUDIO_RESUME, req, &s, sizeof s) != 0) {
|
||||
mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO;
|
||||
}
|
||||
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
|
||||
while (g_pending == req) { if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_TIMEOUT; } }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_audio_close(int sid)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_direct) { g_has_stream = 0; if (g_dir_fd >= 0) close(g_dir_fd); g_dir_fd = -1; pthread_mutex_unlock(&g_mu); return 0; }
|
||||
if (g_has_stream && g_sid == (uint32_t)sid) {
|
||||
m_audio_stream_t s; s.stream_id = (uint32_t)sid;
|
||||
uint32_t req = g_pending + 1; g_pending = req;
|
||||
if (send_frame(g_fd, MSG_AUDIO_STOP, req, &s, sizeof s) == 0) {
|
||||
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
|
||||
while (g_pending == req) { if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) break; }
|
||||
}
|
||||
g_has_stream = 0;
|
||||
g_sid = 0;
|
||||
g_dead = 0;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 从 audiod STATE 文本解析关键字段 */
|
||||
static int state_text_parse(const char *t, meow_audio_state *o)
|
||||
{
|
||||
memset(o, 0, sizeof *o);
|
||||
o->route_port = -1;
|
||||
const char *p;
|
||||
if ((p = strstr(t, "active="))) o->active = atoi(p + 7);
|
||||
if ((p = strstr(t, "app="))) {
|
||||
/* app_id 只取到下一个空白(状态行是空格分隔 k=v,整体拷贝会吞掉后续字段) */
|
||||
p += 4;
|
||||
size_t k = 0;
|
||||
while (p[k] && p[k] != ' ' && p[k] != '\n' && p[k] != '\t' && k < sizeof o->app_id - 1) {
|
||||
o->app_id[k] = p[k]; k++;
|
||||
}
|
||||
o->app_id[k] = 0;
|
||||
}
|
||||
if ((p = strstr(t, "sid="))) o->stream_id = (uint32_t)atoi(p + 4);
|
||||
if ((p = strstr(t, "rate="))) o->rate = (uint32_t)atoi(p + 5);
|
||||
if ((p = strstr(t, "ch="))) o->ch = (uint16_t)atoi(p + 3);
|
||||
if ((p = strstr(t, "fmt="))) {
|
||||
int f = 0;
|
||||
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;
|
||||
o->fmt = (uint16_t)f;
|
||||
}
|
||||
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);
|
||||
if ((p = strstr(t, "paused="))) o->paused = atoi(p + 7);
|
||||
if ((p = strstr(t, "port="))) o->route_port = atoi(p + 5);
|
||||
if ((p = strstr(t, "vol="))) o->vol = atoi(p + 4);
|
||||
if (o->app_id[0] == '\0') snprintf(o->app_id, sizeof o->app_id, "-");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_audio_query(meow_audio_state *out)
|
||||
{
|
||||
if (!out) return MEOW_ERR_INVAL;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_direct) {
|
||||
memset(out, 0, sizeof *out);
|
||||
out->active = g_has_stream;
|
||||
out->stream_id = g_sid;
|
||||
out->rate = (uint32_t)g_grant_rate;
|
||||
out->ch = (uint16_t)g_grant_ch;
|
||||
out->fmt = (uint16_t)g_grant_fmt;
|
||||
out->route_port = -1;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
int fd = conn_locked();
|
||||
if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; }
|
||||
uint32_t req = g_pending + 1; g_pending = req;
|
||||
if (send_frame(g_fd, MSG_AUDIO_QUERY, req, NULL, 0) != 0) { mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; }
|
||||
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
|
||||
while (g_pending == req) { if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_TIMEOUT; } }
|
||||
if (g_resp_type != MSG_AUDIO_STATE) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO; }
|
||||
g_resp[g_resp_len < MEOW_IPC_MAX - 1 ? g_resp_len : MEOW_IPC_MAX - 1] = 0;
|
||||
state_text_parse((const char *)g_resp, out);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_preempt_cb = cb;
|
||||
g_preempt_ctx = ctx;
|
||||
int need = !g_thr_on && !g_direct;
|
||||
if (need) { g_quit = 0; g_thr_on = 1; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (need) { pthread_create(&g_thr, NULL, audio_thread_main, NULL); pthread_detach(g_thr); }
|
||||
}
|
||||
|
||||
/* 读线程主体:poll 读 audiod 下发帧 → 分发 PREEMPTED / 挂起响应。
|
||||
* **不**自行 connect——连接由调用方(open/query 经 conn_locked)建立并启动本线程;
|
||||
* 这里只等 g_fd 就绪后 poll(避免与调用方 conn_locked 竞争出新 socket)。 */
|
||||
static void *audio_thread_main(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!g_quit) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int fd = g_fd;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (fd < 0) { usleep(300000); continue; }
|
||||
|
||||
struct pollfd p;
|
||||
p.fd = fd; p.events = POLLIN; p.revents = 0;
|
||||
int pr = poll(&p, 1, 200);
|
||||
if (pr < 0 && errno != EINTR) {
|
||||
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
|
||||
continue;
|
||||
}
|
||||
if (pr > 0 && (p.revents & (POLLHUP | POLLERR | POLLNVAL))) {
|
||||
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
|
||||
continue;
|
||||
}
|
||||
if (pr > 0 && (p.revents & POLLIN)) {
|
||||
uint8_t rbuf[MEOW_IPC_MAX];
|
||||
uint16_t type;
|
||||
int n = read_frame(fd, &type, rbuf, sizeof rbuf);
|
||||
if (n < 0) { pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu); continue; }
|
||||
if (type == MSG_AUDIO_PREEMPTED && n >= (int)sizeof(m_audio_preempted_t)) {
|
||||
m_audio_preempted_t pm;
|
||||
memcpy(&pm, rbuf, sizeof pm);
|
||||
meow_audio_preempt_fn cb;
|
||||
void *ctx;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
cb = g_preempt_cb; ctx = g_preempt_ctx;
|
||||
if (pm.stream_id == g_sid) { g_has_stream = 0; g_dead = 1; g_sid = 0; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (cb) cb(pm.by_app, ctx);
|
||||
} else if (type == MSG_AUDIO_OPEN_ACK || type == MSG_ACK || type == MSG_AUDIO_STATE) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_pending) {
|
||||
g_resp_type = type;
|
||||
if ((uint32_t)n > MEOW_IPC_MAX) n = (int)MEOW_IPC_MAX;
|
||||
memcpy(g_resp, rbuf, (size_t)n);
|
||||
g_resp_len = (uint32_t)n;
|
||||
g_pending = 0;
|
||||
pthread_cond_signal(&g_cond);
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
/* 其它类型丢弃 */
|
||||
}
|
||||
}
|
||||
pthread_mutex_lock(&g_mu);
|
||||
mark_dead_locked();
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* meow_ui_config.c — MeowOS 应用配置读写 API(M2.x,§9.2)。
|
||||
*
|
||||
* 背景:第三/系统 App 需要读写「自己的配置」(播放列表偏好、UI 选项、最近列表等)。
|
||||
* 此前 SDK 无统一接口,App 只能自己造轮子(自读 env + snprintf + fopen/fprintf),
|
||||
* 重复、易错、无失败降级。本文件实现 SDK 统一 meow_config_*:
|
||||
*
|
||||
* - app 一行 meow_config_set/get 读写键值,SDK 管「数据目录解析 / 文件布局 / 降级」;
|
||||
* - 数据目录:优先 env MEOW_DATA_DIR(launcher 注入 /usr/data/mnt/sd_0/MeowData/<pkg>/data,
|
||||
* 见 app_launcher.c launch_app,exFAT 无 symlink → env 传址);无 env(手动跑/宿主测/无卡)
|
||||
* 回退 /tmp/meowcfg_<pid>(非持久但仍可写);
|
||||
* - 文件布局:每键一小文件 <data>/<sanitized_key>.cfg,内容=值字符串(空串=0 字节文件);
|
||||
* key 只允许 [A-Za-z0-9._-],其余字符(含 '/')替换为 '_',杜绝路径穿越/子目录注入;
|
||||
* - 轻量:每次 get 读文件(App 配置小),不做大内存缓存;pthread_once 惰性初始化目录;
|
||||
* - 线程安全:pthread_mutex 串行库内单键操作(不同 key 独立文件,无共享读写竞态);
|
||||
* - 失败降级:无数据目录/建目录失败/读写失败 → 返回负错误码(MEOW_CFG_ERR_*),不崩不阻塞。
|
||||
*
|
||||
* 与 meow_settings_*(系统设置)的界限:settings 是「系统/MeowOS 设置」(板载
|
||||
* /usr/data/meow/settings.conf、带 schema/取值域/枚举/listener),config 是「App 自有 KV」
|
||||
* (TF 数据目录、raw 字符串、无 schema)。二者命名/落点/语义不同层;本文件**不 import**
|
||||
* meow_settings,纯 libc 完成,可宿主 gcc 直接编译单测。
|
||||
*
|
||||
* 约定:与 meow_ui_log.c 同风格(M2.x 中文注释 + pthread_once 惰性 init);纯 libc,零依赖。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* 内部常量(轻量键值小文件,见头注释) */
|
||||
#define MEOW_CFG_DIR_MAX 320 /* 数据目录绝对路径上限 */
|
||||
#define MEOW_CFG_KEY_MAX 64 /* key 上限(清洗后) */
|
||||
#define MEOW_CFG_VAL_MAX 4096 /* 值上限(set 截断;get 按 outsz-1 读) */
|
||||
#define MEOW_CFG_PATH_MAX (MEOW_CFG_DIR_MAX + MEOW_CFG_KEY_MAX + 8) /* <dir>/<key>.cfg + NUL */
|
||||
|
||||
/* 内部状态(各字段约定见头注释) */
|
||||
static pthread_mutex_t g_cfg_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_once_t g_cfg_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
static char g_cfg_dir[MEOW_CFG_DIR_MAX] = ""; /* 解析后的数据目录(env 或 /tmp 回退) */
|
||||
static int g_cfg_ready = 0; /* 1=目录已解析且 mkdir 成功 */
|
||||
static int g_cfg_env = 0; /* 1=来自 MEOW_DATA_DIR(持久),0=/tmp 回退 */
|
||||
|
||||
/* mkdir -p:逐分量 mkdir(已存在 EEXIST 视为成功)。返回 0=成功,-1=失败。
|
||||
* 仿 meow_ui_log.c log_mkdir_p;用于数据目录不存在时补齐(语义幂等)。 */
|
||||
static int cfg_mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MEOW_CFG_DIR_MAX];
|
||||
size_t len, i;
|
||||
if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1;
|
||||
len = strlen(path);
|
||||
memcpy(tmp, path, len + 1);
|
||||
if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; /* 去尾斜杠 */
|
||||
for (i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 一次性初始化(pthread_once 保证仅一次):解析数据目录 + mkdir -p + 置 ready。 */
|
||||
static void cfg_init(void)
|
||||
{
|
||||
const char *env = getenv("MEOW_DATA_DIR");
|
||||
|
||||
/* 层序:1) env MEOW_DATA_DIR(launcher 注入,持久);2) 回退 /tmp/meowcfg_<pid>。 */
|
||||
if (env && env[0]) {
|
||||
snprintf(g_cfg_dir, sizeof g_cfg_dir, "%s", env);
|
||||
g_cfg_env = 1;
|
||||
} else {
|
||||
snprintf(g_cfg_dir, sizeof g_cfg_dir, "/tmp/meowcfg_%d", (int)getpid());
|
||||
g_cfg_env = 0;
|
||||
}
|
||||
|
||||
/* 去尾斜杠(统一 <dir>/<key>.cfg 拼接) */
|
||||
size_t n = strlen(g_cfg_dir);
|
||||
while (n > 1 && g_cfg_dir[n - 1] == '/') g_cfg_dir[--n] = '\0';
|
||||
|
||||
/* mkdir 幂等;失败只降级(g_cfg_ready=0 → meow_config_dir 返回 NULL),不崩。
|
||||
* 注:env 指向 <pkg>/data 时 launcher 已建该目录;卡不在/只读 → 此处失败降级。 */
|
||||
if (cfg_mkdir_p(g_cfg_dir) == 0) g_cfg_ready = 1;
|
||||
}
|
||||
|
||||
/* key 清洗:只保留 [A-Za-z0-9._-],其余(含 '/'、空白、'..')逐字符替换为 '_'。
|
||||
* 返回 0=成功(写入 out),-1=空 key/全非法。清洗后无路径分隔,杜绝路径穿越。 */
|
||||
static int cfg_sanitize_key(const char *key, char *out, int n)
|
||||
{
|
||||
int len = 0;
|
||||
const unsigned char *p;
|
||||
if (!key || n <= 0) return -1;
|
||||
for (p = (const unsigned char *)key; *p && len < n - 1; p++) {
|
||||
unsigned char c = *p;
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-')
|
||||
out[len++] = (char)c;
|
||||
else
|
||||
out[len++] = '_';
|
||||
}
|
||||
if (len == 0) return -1; /* 全非法(如纯 '/')→ 拒绝 */
|
||||
out[len] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 拼 <dir>/<key>.cfg 到 path。返回 0=成功,-1=路径超长。 */
|
||||
static int cfg_path(const char *key, char *path, int n)
|
||||
{
|
||||
const char *d = meow_config_dir();
|
||||
if (!d) return -1;
|
||||
if (snprintf(path, (size_t)n, "%s/%s.cfg", d, key) >= n) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *meow_config_dir(void)
|
||||
{
|
||||
/* 不取 g_cfg_mu:pthread_once 保证 cfg_init 只跑一次,且其完成后 g_cfg_dir/g_cfg_ready
|
||||
* 已是稳定快照(once 自带 happens-before/内存序)。set/get/remove 持锁后经 cfg_path 也调
|
||||
* 本函数——若在此加锁会自我死锁(非递归互斥)。故仅返回只读快照。 */
|
||||
pthread_once(&g_cfg_once, cfg_init);
|
||||
return g_cfg_ready ? g_cfg_dir : NULL;
|
||||
}
|
||||
|
||||
int meow_config_set(const char *key, const char *val)
|
||||
{
|
||||
char k[MEOW_CFG_KEY_MAX + 1];
|
||||
char path[MEOW_CFG_PATH_MAX];
|
||||
const char *v = val ? val : ""; /* NULL 视为空串(=清空该键) */
|
||||
size_t vlen, towrite;
|
||||
int fd, rc = MEOW_CFG_OK, s;
|
||||
|
||||
if (cfg_sanitize_key(key, k, sizeof k) != 0) return MEOW_CFG_ERR_INVAL;
|
||||
vlen = strlen(v);
|
||||
towrite = vlen > MEOW_CFG_VAL_MAX ? MEOW_CFG_VAL_MAX : vlen; /* 值上限截断 */
|
||||
|
||||
pthread_mutex_lock(&g_cfg_mu);
|
||||
s = cfg_path(k, path, sizeof path);
|
||||
if (s != 0) { pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_NOMEM; }
|
||||
|
||||
fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) {
|
||||
if (!g_cfg_env) fprintf(stderr, "meow_config: set '%s' open %s: %s\n", k, path, strerror(errno));
|
||||
pthread_mutex_unlock(&g_cfg_mu);
|
||||
return MEOW_CFG_ERR_IO;
|
||||
}
|
||||
/* 循环写完(防短写;配置小,实际单次 write 即完) */
|
||||
size_t off = 0;
|
||||
while (off < towrite) {
|
||||
ssize_t w = write(fd, v + off, towrite - off);
|
||||
if (w < 0) { rc = MEOW_CFG_ERR_IO; break; }
|
||||
if (w == 0) { rc = MEOW_CFG_ERR_IO; break; }
|
||||
off += (size_t)w;
|
||||
}
|
||||
close(fd);
|
||||
pthread_mutex_unlock(&g_cfg_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int meow_config_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
char k[MEOW_CFG_KEY_MAX + 1];
|
||||
char path[MEOW_CFG_PATH_MAX];
|
||||
ssize_t n;
|
||||
|
||||
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||
out[0] = '\0';
|
||||
if (cfg_sanitize_key(key, k, sizeof k) != 0) return MEOW_CFG_ERR_INVAL;
|
||||
|
||||
pthread_mutex_lock(&g_cfg_mu);
|
||||
if (cfg_path(k, path, sizeof path) != 0) { pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_NOMEM; }
|
||||
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) { pthread_mutex_unlock(&g_cfg_mu); return MEOW_CFG_ERR_NOENT; } /* 未设/不存在 */
|
||||
|
||||
n = read(fd, out, (size_t)outsz - 1);
|
||||
if (n < 0) n = 0;
|
||||
close(fd);
|
||||
pthread_mutex_unlock(&g_cfg_mu);
|
||||
|
||||
out[n] = '\0'; /* 截断/短读均安全 NUL 终止 */
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_int(const char *key, int def)
|
||||
{
|
||||
char buf[32];
|
||||
char *end = NULL;
|
||||
long v;
|
||||
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
|
||||
v = strtol(buf, &end, 10);
|
||||
if (end == buf) return def; /* 非数字 → def */
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
int meow_config_bool(const char *key, int def)
|
||||
{
|
||||
char buf[16];
|
||||
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
|
||||
if (!strcmp(buf, "1") || !strcmp(buf, "true") || !strcmp(buf, "yes") || !strcmp(buf, "on"))
|
||||
return 1;
|
||||
if (!strcmp(buf, "0") || !strcmp(buf, "false") || !strcmp(buf, "no") || !strcmp(buf, "off"))
|
||||
return 0;
|
||||
return def;
|
||||
}
|
||||
|
||||
void meow_config_remove(const char *key)
|
||||
{
|
||||
char k[MEOW_CFG_KEY_MAX + 1];
|
||||
char path[MEOW_CFG_PATH_MAX];
|
||||
|
||||
if (cfg_sanitize_key(key, k, sizeof k) != 0) return; /* 非法 key 静默 */
|
||||
|
||||
pthread_mutex_lock(&g_cfg_mu);
|
||||
if (cfg_path(k, path, sizeof path) == 0)
|
||||
unlink(path); /* 无目录 / 文件不存在:unlink 失败即静默(void 语义) */
|
||||
pthread_mutex_unlock(&g_cfg_mu);
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* meow_ui_dialog.c — §5.8.6 模态控件化(App 层,非系统叠加):Dialog / BottomSheet。
|
||||
*
|
||||
* 复用 §5.6 B③ 弹窗 / §5.7 B④ 底部面板模板(与 launcher s_dd_modal 同构):遮罩压暗 +
|
||||
* 圆角卡片 + 标题 + 内容 + 操作;返回手势=取消;不压状态栏。阻塞式(内层 meow_input_pop
|
||||
* 轮询,与 meow_ui_text_input 同构),打开前排空残留触摸(goodix 长尾免疫)。
|
||||
* 命中一律用抬手最新坐标;即画即显(打开一次性绘制,仅高亮变化才重绘,逐帧不重画静态内容)。
|
||||
*
|
||||
* 属 App 层模态:不注册进系统顶条/通知中心渲染(系统级控件归属不动)。物理键在 B1 未下发
|
||||
* 给 App(on_key 无键),故本层关闭途径=触摸 + 左缘返回手势;音量/电源高亮导航为后续批次。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define MEOW_MODAL_MAX_ITEMS 32
|
||||
#define MEOW_MODAL_ROW 48
|
||||
#define MEOW_MODAL_BTN 44
|
||||
|
||||
/* M2.71-bug3:模态(Dialog/BottomSheet)背景快照/恢复。
|
||||
* 旧实现直接往 back_buf 画 scrim+卡片并 flush:①每次 open 再现叠一层 α150 遮罩(叠加变黑);
|
||||
* ②关闭后 scrim+卡片像素留在屏上(残影);③要整屏刷新才盖掉。此处 open 前把整帧背景拍下,
|
||||
* 关闭后回填 + 整帧上屏 → scrim 每次只叠一层、关闭即还原、无残留。 */
|
||||
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)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
int line = meow_ui_back_buf_pitch();
|
||||
int h = meow_ui_height();
|
||||
int bpp = meow_ui_back_buf_bpp();
|
||||
if (!bb || line <= 0 || h <= 0) { g_modal_cap = NULL; return; }
|
||||
size_t n = (size_t)line * (size_t)h;
|
||||
if (!g_modal_cap || g_modal_cap_line != line || g_modal_cap_h != h || g_modal_cap_bpp != bpp) {
|
||||
free(g_modal_cap);
|
||||
g_modal_cap = (unsigned char *)malloc(n);
|
||||
g_modal_cap_line = line; g_modal_cap_h = h; g_modal_cap_bpp = bpp;
|
||||
}
|
||||
if (g_modal_cap) memcpy(g_modal_cap, bb, n);
|
||||
}
|
||||
|
||||
static void modal_restore(void)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
if (!g_modal_cap || !bb) return;
|
||||
size_t n = (size_t)g_modal_cap_line * (size_t)g_modal_cap_h;
|
||||
memcpy(bb, g_modal_cap, n);
|
||||
meow_gfx_mark_full();
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
/* ---- Dialog ---- */
|
||||
struct meow_dialog {
|
||||
meow_dlg_t type;
|
||||
char title[96];
|
||||
char items[MEOW_MODAL_MAX_ITEMS][64];
|
||||
int nitems;
|
||||
int cur; /* SELECT 预选/高亮项 */
|
||||
int result;
|
||||
int done;
|
||||
meow_ime_ctx ime; /* INPUT 型(拷贝) */
|
||||
int has_ime;
|
||||
meow_ime_ctx *ime_orig; /* INPUT:App 传入的原始 ctx(回写最终文本,App 关闭后可读回) */
|
||||
};
|
||||
|
||||
/* 卡片几何:返回标题之后内容起点(行/按钮相对坐标)。 */
|
||||
typedef struct { int dx,dy,dw,dh; int row_y,rowh; int btn_y,btn_w,btn_h; } dlg_geom;
|
||||
|
||||
static void dlg_calc(meow_dialog *d, const meow_theme *th, dlg_geom *g)
|
||||
{
|
||||
int W = meow_ui_width(), H = meow_ui_height();
|
||||
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
|
||||
int pad = 24;
|
||||
int tH = meow_ui_text_height(th->font_title);
|
||||
int bH = meow_ui_text_height(th->font_body);
|
||||
int select = (d->type == MEOW_DLG_SELECT);
|
||||
int nrows = select ? d->nitems : 0;
|
||||
int nmsg = (d->type == MEOW_DLG_INFO) ? (d->nitems > 0 ? d->nitems : 1)
|
||||
: (select ? 0 : (d->nitems > 0 ? d->nitems : 1));
|
||||
int have_btns = !select;
|
||||
g->dw = 348; if (g->dw > W - 40) g->dw = W - 40;
|
||||
g->dx = (W - g->dw) / 2;
|
||||
int content = nrows * MEOW_MODAL_ROW + nmsg * (bH + 8);
|
||||
g->dh = pad + tH + 12 + content + pad + (have_btns ? 16 + MEOW_MODAL_BTN : 0);
|
||||
int avail = H - sb - 20;
|
||||
if (g->dh > avail) g->dh = avail;
|
||||
g->dy = (H - g->dh) / 2; if (g->dy < sb) g->dy = sb;
|
||||
g->row_y = g->dy + pad + tH + 12;
|
||||
g->rowh = (MEOW_MODAL_ROW < 0) ? 40 : MEOW_MODAL_ROW;
|
||||
g->btn_y = g->row_y + content + 16;
|
||||
g->btn_h = MEOW_MODAL_BTN;
|
||||
g->btn_w = (g->dw - 2 * pad - 12) / 2;
|
||||
}
|
||||
|
||||
/* 只画卡片内容(不含遮罩)——供「仅高亮变化」时重绘卡片区,不会把遮罩再叠一层(防叠加变黑)。 */
|
||||
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_ui_text(g->dx + pad, g->dy + pad, d->title, th->font_title, th->onSurface);
|
||||
|
||||
if (d->type == MEOW_DLG_SELECT) {
|
||||
for (int i = 0; i < d->nitems; i++) {
|
||||
int ry = g->row_y + i * g->rowh;
|
||||
if (ry + g->rowh > g->dy + g->dh - 12) break;
|
||||
if (i == d->cur) {
|
||||
meow_ui_fill_rect(g->dx + 8, ry, g->dw - 16, g->rowh, th->primary);
|
||||
meow_ui_text(g->dx + pad, ry + (g->rowh - meow_ui_text_height(th->font_body)) / 2,
|
||||
d->items[i], th->font_body, 0xFFFFFF);
|
||||
} else {
|
||||
meow_ui_text(g->dx + pad, ry + (g->rowh - meow_ui_text_height(th->font_body)) / 2,
|
||||
d->items[i], th->font_body, th->onSurface);
|
||||
}
|
||||
meow_ui_fill_rect(g->dx + pad, ry + g->rowh - 1, g->dw - 2 * pad, 1, th->outlineVariant);
|
||||
}
|
||||
} else {
|
||||
/* INFO/CONFIRM:正文行;INPUT:文本预览 */
|
||||
int nmsg = (d->type == MEOW_DLG_INFO) ? (d->nitems > 0 ? d->nitems : 1)
|
||||
: (d->nitems > 0 ? d->nitems : 1);
|
||||
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...)");
|
||||
meow_ui_text(g->dx + pad, y, buf, th->font_body, th->onSurfaceVariant);
|
||||
} else {
|
||||
for (int i = 0; i < nmsg; i++) {
|
||||
if (i < d->nitems && d->items[i][0])
|
||||
meow_ui_text(g->dx + pad, y, d->items[i], th->font_body, th->onSurfaceVariant);
|
||||
y += meow_ui_text_height(th->font_body) + 8;
|
||||
}
|
||||
}
|
||||
/* 取消(左,text 型)/ 确定(右,filled) */
|
||||
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_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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 整帧绘制:遮罩 + 卡片。 */
|
||||
static void dlg_paint(meow_dialog *d, const meow_theme *th)
|
||||
{
|
||||
meow_ui_fill_rect_alpha(0, 0, meow_ui_width(), meow_ui_height(), 0x000000, 150); /* 遮罩压暗 */
|
||||
dlg_geom g;
|
||||
dlg_calc(d, th, &g);
|
||||
dlg_card(d, th, &g);
|
||||
}
|
||||
|
||||
/* 仅重绘卡片区(遮罩不变),flush 卡片矩形:高亮变化用,不叠加遮罩。 */
|
||||
static void dlg_repaint(meow_dialog *d, const meow_theme *th)
|
||||
{
|
||||
dlg_geom g;
|
||||
dlg_calc(d, th, &g);
|
||||
dlg_card(d, th, &g);
|
||||
meow_ui_flush_region(g.dx, g.dy, g.dw, g.dh);
|
||||
}
|
||||
|
||||
/* M2.71-IME-fix:dialog INPUT 的 on_change——实时把 IME 文本回写进 d->ime.buf(dialog 内部拷贝),
|
||||
* 并重绘卡片预览区,使用户边打字边看到当前文本。ESC/点背景(result=0)关闭后 d->ime.buf 仍是
|
||||
* 最新文本(否则 meow_ime_close(0) 不回写 buf → 文本丢失)。d->ime.buf 是 dialog 内部缓冲
|
||||
* (非 App 持有);App 的最终文本经 ime_orig 在关闭时回写(见 OK 按钮处理)。 */
|
||||
static void dlg_ime_change_cb(const char *text, void *ctx)
|
||||
{
|
||||
meow_dialog *d = (meow_dialog *)ctx;
|
||||
if (!d) return;
|
||||
if (text) {
|
||||
strncpy(d->ime.buf, text, sizeof d->ime.buf - 1);
|
||||
d->ime.buf[sizeof d->ime.buf - 1] = 0;
|
||||
}
|
||||
const meow_theme *th = meow_theme_get();
|
||||
dlg_repaint(d, th);
|
||||
}
|
||||
|
||||
/* 准备好 dialog 的 IME 上下文:无论 App 传入(set_ime)或内部兜底(show 的默认 ctx),
|
||||
* 都由 dialog 接管 on_change/ctx,以支持 dlg_ime_change_cb 的实时回写。 */
|
||||
static void dlg_prep_ime(meow_dialog *d)
|
||||
{
|
||||
d->ime.on_change = dlg_ime_change_cb;
|
||||
d->ime.ctx = d;
|
||||
}
|
||||
|
||||
meow_dialog *meow_dialog_create(meow_dlg_t t, const char *title)
|
||||
{
|
||||
meow_dialog *d = (meow_dialog *)calloc(1, sizeof *d);
|
||||
if (!d) return NULL;
|
||||
d->type = t;
|
||||
if (title) { strncpy(d->title, title, sizeof d->title - 1); d->title[sizeof d->title - 1] = 0; }
|
||||
d->cur = -1; d->result = 0; d->done = 0;
|
||||
return d;
|
||||
}
|
||||
void meow_dialog_set_items(meow_dialog *d, const char *const *item, int n, int cur)
|
||||
{
|
||||
if (!d) return;
|
||||
d->nitems = 0;
|
||||
if (item) for (int i = 0; i < n && i < MEOW_MODAL_MAX_ITEMS; i++)
|
||||
if (item[i]) { strncpy(d->items[d->nitems], item[i], 63); d->items[d->nitems][63] = 0; d->nitems++; }
|
||||
d->cur = cur;
|
||||
}
|
||||
void meow_dialog_set_ime(meow_dialog *d, meow_ime_ctx *ime)
|
||||
{
|
||||
if (!d || !ime) return;
|
||||
d->ime = *ime;
|
||||
d->ime_orig = ime; /* M2.71-IME-fix:记住 App 原始 ctx,关闭时回写最终文本 */
|
||||
d->has_ime = 1;
|
||||
dlg_prep_ime(d); /* dialog 接管 on_change:实时回写 d->ime.buf + 重绘卡片 */
|
||||
}
|
||||
|
||||
void meow_dialog_close(meow_dialog *d)
|
||||
{
|
||||
if (d) d->done = 1;
|
||||
}
|
||||
|
||||
void meow_dialog_show(meow_dialog *d)
|
||||
{
|
||||
if (!d) return;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
/* INPUT 型若未带 ime,用空 ctx 兜底 */
|
||||
if (d->type == MEOW_DLG_INPUT && !d->has_ime) {
|
||||
memset(&d->ime, 0, sizeof d->ime);
|
||||
d->ime.maxlen = 255;
|
||||
d->ime_orig = NULL; /* 无 App 原始 ctx,关闭时不回写 */
|
||||
dlg_prep_ime(d);
|
||||
}
|
||||
meow_input_start();
|
||||
meow_input_drain(); /* 排空残留触摸(goodix 长尾) */
|
||||
d->done = 0; d->result = 0;
|
||||
modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */
|
||||
dlg_paint(d, th);
|
||||
meow_ui_flush();
|
||||
|
||||
while (!d->done) {
|
||||
meow_input_ev e;
|
||||
while (meow_input_pop(&e)) {
|
||||
meow_ui_activity_ping(); /* M2.71-bug5:模态期间交互也重置息屏计时 */
|
||||
if (e.type == MEOW_EV_TAP) {
|
||||
int x = e.a, y = e.b;
|
||||
dlg_geom g;
|
||||
dlg_calc(d, th, &g);
|
||||
int in_card = x >= g.dx && x < g.dx + g.dw && y >= g.dy && y < g.dy + g.dh;
|
||||
if (!in_card) { d->done = 1; d->result = 0; break; } /* 遮罩外=取消 */
|
||||
if (d->type == MEOW_DLG_SELECT) {
|
||||
/* M2.71-bug3:点项 → 立即刷「选中」高亮(不下线、不叠加遮罩);
|
||||
* 再点同一项 → 确认(返回该项下标)。点卡片外=取消。 */
|
||||
int idx = (y - g.row_y) / g.rowh;
|
||||
if (idx >= 0 && idx < d->nitems) {
|
||||
if (idx == d->cur) { d->result = idx; d->done = 1; }
|
||||
else { d->cur = idx; dlg_repaint(d, th); }
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* 非选择:按钮命中 */
|
||||
int pad = 24, by = g.btn_y;
|
||||
int bx = g.dx + pad, cx = bx + g.btn_w + 12;
|
||||
if (y >= by && y < by + g.btn_h) {
|
||||
if (x >= bx && x < bx + g.btn_w) { /* CANCEL */
|
||||
d->done = 1; d->result = 0;
|
||||
} else if (x >= cx && x < cx + g.btn_w) { /* OK */
|
||||
if (d->type == MEOW_DLG_INPUT) {
|
||||
d->result = meow_ui_text_input(&d->ime); /* 阻塞编辑 */
|
||||
/* M2.71-IME-fix:回车只提交不关(键盘仍开);文本经 on_change 已实时
|
||||
* 写进 d->ime.buf。这里无论 result=0/1 都把最终文本回写 App 原始 ctx,
|
||||
* 保证 ESC/点背景(result=0)关闭后 App 仍能读到编辑结果(不再丢失)。 */
|
||||
if (d->ime_orig) {
|
||||
strncpy(d->ime_orig->buf, d->ime.buf, sizeof d->ime_orig->buf - 1);
|
||||
d->ime_orig->buf[sizeof d->ime_orig->buf - 1] = 0;
|
||||
}
|
||||
} else {
|
||||
d->result = (d->type == MEOW_DLG_CONFIRM) ? 1 : 1;
|
||||
}
|
||||
d->done = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (e.type == MEOW_EV_SWIPE && e.a == MEOW_SWIPE_LEFT) {
|
||||
d->done = 1; d->result = 0; /* 返回=取消 */
|
||||
}
|
||||
}
|
||||
if (d->done) break;
|
||||
usleep(16000);
|
||||
}
|
||||
modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
|
||||
}
|
||||
|
||||
int meow_dialog_result(meow_dialog *d) { return d ? d->result : 0; }
|
||||
int meow_dialog_confirm(meow_dialog *d)
|
||||
{
|
||||
if (!d || d->type != MEOW_DLG_CONFIRM) return 0;
|
||||
meow_dialog_show(d);
|
||||
return d->result == 1;
|
||||
}
|
||||
void meow_dialog_free(meow_dialog *d) { if (d) free(d); }
|
||||
|
||||
/* ---- BottomSheet ---- */
|
||||
struct meow_sheet {
|
||||
char title[96];
|
||||
char items[MEOW_MODAL_MAX_ITEMS][64];
|
||||
int nitems;
|
||||
int h;
|
||||
int result;
|
||||
int done;
|
||||
};
|
||||
|
||||
static int sheet_h(meow_sheet *s, const meow_theme *th)
|
||||
{
|
||||
int H = meow_ui_height();
|
||||
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
|
||||
int avail = H - sb - 20;
|
||||
int title_h = meow_ui_text_height(th->font_title);
|
||||
int min_h = 24 + title_h + 12 + MEOW_MODAL_ROW + 20;
|
||||
int want = (s->h > 0) ? s->h : min_h;
|
||||
int est = 24 + title_h + 12 + s->nitems * MEOW_MODAL_ROW + 20;
|
||||
if (est > want) want = est;
|
||||
if (want > avail) want = avail;
|
||||
if (want < min_h) want = min_h < avail ? min_h : avail;
|
||||
return want;
|
||||
}
|
||||
|
||||
meow_sheet *meow_sheet_create(const char *title, int h)
|
||||
{
|
||||
meow_sheet *s = (meow_sheet *)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->h = h > 0 ? h : 0;
|
||||
return s;
|
||||
}
|
||||
int meow_sheet_add_item(meow_sheet *s, const char *label)
|
||||
{
|
||||
if (!s || s->nitems >= MEOW_MODAL_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;
|
||||
}
|
||||
int meow_sheet_open(meow_sheet *s)
|
||||
{
|
||||
if (!s) return -1;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_input_start();
|
||||
meow_input_drain();
|
||||
s->done = 0; s->result = -1;
|
||||
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_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);
|
||||
for (int i = 0; i < s->nitems; i++) {
|
||||
int ry = py + pad + meow_ui_text_height(th->font_title) + 12 + i * MEOW_MODAL_ROW;
|
||||
if (ry + MEOW_MODAL_ROW > H - 8) break;
|
||||
meow_ui_text(pad, ry + (MEOW_MODAL_ROW - meow_ui_text_height(th->font_body)) / 2,
|
||||
s->items[i], th->font_body, th->onSurface);
|
||||
meow_ui_fill_rect(pad, ry + MEOW_MODAL_ROW - 1, W - 2 * pad, 1, th->outlineVariant);
|
||||
}
|
||||
meow_ui_flush();
|
||||
|
||||
while (!s->done) {
|
||||
meow_input_ev e;
|
||||
while (meow_input_pop(&e)) {
|
||||
meow_ui_activity_ping(); /* M2.71-bug5:模态期间交互也重置息屏计时 */
|
||||
if (e.type == MEOW_EV_TAP) {
|
||||
int y = e.b;
|
||||
if (y < py) { s->done = 1; s->result = -1; break; } /* 点面板外=取消 */
|
||||
int title_h = meow_ui_text_height(th->font_title);
|
||||
int cy = py + pad + title_h + 12;
|
||||
int idx = (y - cy) / MEOW_MODAL_ROW;
|
||||
if (idx >= 0 && idx < s->nitems) { s->result = idx; s->done = 1; }
|
||||
break;
|
||||
} else if (e.type == MEOW_EV_SWIPE && e.a == MEOW_SWIPE_LEFT) {
|
||||
s->done = 1; s->result = -1;
|
||||
}
|
||||
}
|
||||
if (s->done) break;
|
||||
usleep(16000);
|
||||
}
|
||||
modal_restore(); /* M2.71-bug3:关闭还原背景,无残影 */
|
||||
return s->result;
|
||||
}
|
||||
void meow_sheet_free(meow_sheet *s) { if (s) free(s); }
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* meow_ui_draw.c — 绘制物注册表 / z 序 / 区域重画(§6.2 / SDK_Z_ORDER_DESIGN §2)。
|
||||
*
|
||||
* 非保留模型下,把「清底 + 按 z 重画重叠区完整 z 栈」下沉到 SDK:
|
||||
* - 控件(meow_widget)创建即自动注册(create 末尾调 meow_draw_widget_register),
|
||||
* meow_widget_free 自动注销;rect 实时读 w->x/y/w/h;
|
||||
* - App 直绘的非控件内容显式 meow_draw_custom_register(声明 bbox + 绘制回调);
|
||||
* - z 自动推导(子>父、同层注册先序);显式 set_z 粘滞、置顶。
|
||||
* 显式 z 存于本注册表(按 w 反查 entry),**不改 struct meow_widget**(零 ABI 破坏)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_view.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MEOW_DRAW_MAX 256
|
||||
#define MEOW_ITEM_VIEW 2 /* 内部 kinds(不进公共枚举):meow_view 绘制物 */
|
||||
|
||||
typedef struct draw_entry {
|
||||
int kind; /* MEOW_ITEM_WIDGET / MEOW_ITEM_CUSTOM / MEOW_ITEM_VIEW */
|
||||
meow_widget *w; /* WIDGET:绘制其 meow_widget_draw(w) */
|
||||
meow_draw_fn draw_cb; /* CUSTOM:绘制回调 */
|
||||
void *ctx; /* CUSTOM: 传给 draw_cb; VIEW: (meow_view*)v */
|
||||
int rx, ry, rw, rh; /* CUSTOM bbox(WIDGET 读 w->x/y/w/h,VIEW 读 v->x/y/w/h) */
|
||||
int z; /* 生效 z(显式=其值,否则=自动推导) */
|
||||
int z_explicit; /* 1=显式(粘滞) */
|
||||
int id; /* CUSTOM id(0=无效) */
|
||||
int seq; /* 全局注册序(z 同值时稳定先序) */
|
||||
struct meow_view *view; /* WIDGET:所属 view(z 深度源)+1 */
|
||||
} draw_entry;
|
||||
|
||||
static draw_entry g_e[MEOW_DRAW_MAX];
|
||||
static int g_n = 0;
|
||||
static int g_seq = 0; /* 全局注册序(单调递增) */
|
||||
static int g_next_id = 1; /* CUSTOM id 起点(0=无效) */
|
||||
|
||||
static uint32_t g_page_bg = 0;
|
||||
static int g_page_bg_set = 0;
|
||||
|
||||
/* ---- 内部访问器(widget.c 提供;跨 TU 用 meow_ui_internal.h 声明)---- */
|
||||
static int w_is_layout(const meow_widget *w) { return meow_widget_is_layout(w); }
|
||||
|
||||
/* ---- 查找 ---- */
|
||||
static draw_entry *e_find_widget(const meow_widget *w)
|
||||
{
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (g_e[i].kind == MEOW_ITEM_WIDGET && g_e[i].w == w) return &g_e[i];
|
||||
return NULL;
|
||||
}
|
||||
static draw_entry *e_find_view(const meow_view *v)
|
||||
{
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (g_e[i].kind == MEOW_ITEM_VIEW && g_e[i].ctx == (void *)v) return &g_e[i];
|
||||
return NULL;
|
||||
}
|
||||
static draw_entry *e_find_id(int id)
|
||||
{
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (g_e[i].kind == MEOW_ITEM_CUSTOM && g_e[i].id == id) return &g_e[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- 深度:view 树深度 ---- */
|
||||
static int view_depth(const meow_view *v)
|
||||
{
|
||||
int d = 0;
|
||||
const meow_view *p = v ? v->parent : NULL;
|
||||
while (p) {
|
||||
d++;
|
||||
if (d > MEOW_Z_DEPTH_MAX) break;
|
||||
p = p->parent;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
/* ---- 深度:widget 所属嵌套深度(view 树为主;否则 W_LAYOUT children 反推;否则 0)---- */
|
||||
static int widget_nest_depth(meow_widget *w, int depth)
|
||||
{
|
||||
if (depth > MEOW_Z_DEPTH_MAX) return MEOW_Z_DEPTH_MAX;
|
||||
draw_entry *e = e_find_widget(w);
|
||||
if (e && e->view) return view_depth(e->view) + 1; /* 视图树深度源(更确定) */
|
||||
/* 注册表反推:w 是否经 meow_layout_add 挂在某布局下(此时布局自身已注册为 WIDGET)。 */
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
if (g_e[i].kind != MEOW_ITEM_WIDGET) continue;
|
||||
meow_widget *lw = g_e[i].w;
|
||||
if (!lw || !w_is_layout(lw)) continue;
|
||||
int nc = meow_widget_nchild(lw);
|
||||
for (int j = 0; j < nc; j++)
|
||||
if (meow_widget_child_at(lw, j) == w)
|
||||
return widget_nest_depth(lw, depth + 1) + 1; /* 子层 >= 父层+1 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 计算 auto z(显式不重算——粘滞):depth*STRIDE + 层内 seq(注册先序,折叠 MEOW_Z_PER_LEVEL_MAX)。 */
|
||||
static void e_compute_auto(draw_entry *e)
|
||||
{
|
||||
if (e->z_explicit) return;
|
||||
int depth = 0;
|
||||
if (e->kind == MEOW_ITEM_WIDGET && e->w) depth = widget_nest_depth(e->w, 0);
|
||||
else if (e->kind == MEOW_ITEM_VIEW && e->ctx) depth = view_depth((const meow_view *)e->ctx);
|
||||
/* CUSTOM:z 恒显式(register 传入),不进 auto 分支 */
|
||||
int cnt = 0;
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
draw_entry *o = &g_e[i];
|
||||
if (o == e || o->z_explicit) continue;
|
||||
if (o->seq >= e->seq) continue;
|
||||
if (o->z / MEOW_Z_STRIDE == depth) cnt++;
|
||||
}
|
||||
if (cnt > MEOW_Z_PER_LEVEL_MAX) cnt = MEOW_Z_PER_LEVEL_MAX;
|
||||
e->z = depth * MEOW_Z_STRIDE + cnt;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 控件自动注册 / 注销(create 末尾 / meow_widget_free 调用) */
|
||||
/* ============================================================ */
|
||||
void meow_draw_widget_register(meow_widget *w)
|
||||
{
|
||||
if (!w || e_find_widget(w)) return; /* 幂等 */
|
||||
if (g_n >= MEOW_DRAW_MAX) return; /* 满:静默(骨架级) */
|
||||
draw_entry *e = &g_e[g_n++];
|
||||
memset(e, 0, sizeof *e);
|
||||
e->kind = MEOW_ITEM_WIDGET;
|
||||
e->w = w;
|
||||
e->seq = g_seq++;
|
||||
e_compute_auto(e);
|
||||
}
|
||||
|
||||
void meow_draw_widget_unregister(meow_widget *w)
|
||||
{
|
||||
if (!w) return;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (g_e[i].kind == MEOW_ITEM_WIDGET && g_e[i].w == w) { idx = i; break; }
|
||||
if (idx < 0) return;
|
||||
g_e[idx] = g_e[--g_n]; /* 交换移除(seq 保留,排序稳定) */
|
||||
}
|
||||
|
||||
int meow_draw_widget_registered(const meow_widget *w)
|
||||
{
|
||||
return w ? e_find_widget(w) != NULL : 0;
|
||||
}
|
||||
|
||||
void meow_draw_widget_set_view(meow_widget *w, struct meow_view *v)
|
||||
{
|
||||
draw_entry *e = e_find_widget(w);
|
||||
if (!e) return;
|
||||
e->view = v;
|
||||
e_compute_auto(e);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 显式 z(widget) */
|
||||
/* ============================================================ */
|
||||
void meow_widget_set_z(meow_widget *w, int z)
|
||||
{
|
||||
if (!w || z < 0) return;
|
||||
draw_entry *e = e_find_widget(w);
|
||||
if (!e) return;
|
||||
e->z_explicit = 1;
|
||||
e->z = z;
|
||||
}
|
||||
int meow_widget_z(const meow_widget *w)
|
||||
{
|
||||
if (!w) return 0;
|
||||
draw_entry *e = e_find_widget((meow_widget *)w);
|
||||
if (!e) return 0;
|
||||
if (e->z_explicit) return e->z;
|
||||
e_compute_auto(e);
|
||||
return e->z;
|
||||
}
|
||||
void meow_widget_clear_z(meow_widget *w)
|
||||
{
|
||||
if (!w) return;
|
||||
draw_entry *e = e_find_widget(w);
|
||||
if (!e) return;
|
||||
e->z_explicit = 0;
|
||||
e_compute_auto(e);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* CUSTOM(App 直绘)注册 / 注销 / 改 z */
|
||||
/* ============================================================ */
|
||||
int meow_draw_custom_register(int z, int x, int y, int w, int h,
|
||||
meow_draw_fn cb, void *ctx)
|
||||
{
|
||||
if (!cb || w <= 0 || h <= 0) return 0;
|
||||
if (g_n >= MEOW_DRAW_MAX) return 0;
|
||||
draw_entry *e = &g_e[g_n++];
|
||||
memset(e, 0, sizeof *e);
|
||||
e->kind = MEOW_ITEM_CUSTOM;
|
||||
e->draw_cb = cb;
|
||||
e->ctx = ctx;
|
||||
e->rx = x; e->ry = y; e->rw = w; e->rh = h;
|
||||
e->z = z < 0 ? 0 : z;
|
||||
e->z_explicit = 1;
|
||||
e->id = g_next_id++;
|
||||
e->seq = g_seq++;
|
||||
return e->id;
|
||||
}
|
||||
void meow_draw_unregister(int id)
|
||||
{
|
||||
if (id <= 0) return;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (g_e[i].kind == MEOW_ITEM_CUSTOM && g_e[i].id == id) { idx = i; break; }
|
||||
if (idx < 0) return;
|
||||
g_e[idx] = g_e[--g_n];
|
||||
}
|
||||
int meow_draw_set_z(int id, int z)
|
||||
{
|
||||
if (id <= 0 || z < 0) return -1;
|
||||
draw_entry *e = e_find_id(id);
|
||||
if (!e) return -1;
|
||||
e->z = z;
|
||||
e->z_explicit = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* view 绘制物注册 / 注销 */
|
||||
/* ============================================================ */
|
||||
void meow_draw_view_register(struct meow_view *v)
|
||||
{
|
||||
if (!v || e_find_view(v)) return;
|
||||
if (g_n >= MEOW_DRAW_MAX) return;
|
||||
draw_entry *e = &g_e[g_n++];
|
||||
memset(e, 0, sizeof *e);
|
||||
e->kind = MEOW_ITEM_VIEW;
|
||||
e->ctx = (void *)v;
|
||||
e->seq = g_seq++;
|
||||
e_compute_auto(e);
|
||||
}
|
||||
void meow_draw_view_unregister(struct meow_view *v)
|
||||
{
|
||||
if (!v) return;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (g_e[i].kind == MEOW_ITEM_VIEW && g_e[i].ctx == (void *)v) { idx = i; break; }
|
||||
if (idx < 0) return;
|
||||
g_e[idx] = g_e[--g_n];
|
||||
}
|
||||
int meow_draw_view_entered(const struct meow_view *v)
|
||||
{
|
||||
/* 子>父:view 或其任意祖先已进入即视为「可绘制」——配合 view_enter/leave 递归
|
||||
* (meow_ui_view.c §A.4),父 on_enter 重绘时子视图(尚未单独进入)也要经 on_draw 画出;
|
||||
* 且子随父进出("子视图随父进出"),祖先已进入 ⇒ 子树已进入,可绘制。 */
|
||||
const struct meow_view *cur = v;
|
||||
while (cur) {
|
||||
if (meow_view_get_state(cur) & MEOW_VIEW_STATE_ENTERED) return 1;
|
||||
cur = meow_view_parent(cur);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void meow_draw_view_set_z(struct meow_view *v, int z)
|
||||
{
|
||||
draw_entry *e = e_find_view(v);
|
||||
if (!e) return;
|
||||
if (z <= 0) {
|
||||
e->z_explicit = 0;
|
||||
e_compute_auto(e);
|
||||
} else {
|
||||
e->z_explicit = 1;
|
||||
e->z = z;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* page bg / 区域重画 / 整帧 */
|
||||
/* ============================================================ */
|
||||
void meow_ui_set_page_bg(uint32_t color) { g_page_bg = color; g_page_bg_set = 1; }
|
||||
uint32_t meow_ui_page_bg(void) { return g_page_bg_set ? g_page_bg : meow_theme_get()->bg; }
|
||||
|
||||
static int rect_hit(int ax, int ay, int aw, int ah, int bx, int by, int bw, int bh)
|
||||
{
|
||||
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
||||
}
|
||||
|
||||
static int entry_rect(draw_entry *e, int out[4])
|
||||
{
|
||||
if (e->kind == MEOW_ITEM_WIDGET && e->w) {
|
||||
meow_widget_rect(e->w, out);
|
||||
} else if (e->kind == MEOW_ITEM_VIEW && e->ctx) {
|
||||
meow_view *v = (meow_view *)e->ctx;
|
||||
out[0] = v->x; out[1] = v->y; out[2] = v->w; out[3] = v->h;
|
||||
} else {
|
||||
out[0] = e->rx; out[1] = e->ry; out[2] = e->rw; out[3] = e->rh;
|
||||
}
|
||||
return out[2] > 0 && out[3] > 0;
|
||||
}
|
||||
|
||||
static void draw_one(draw_entry *e)
|
||||
{
|
||||
switch (e->kind) {
|
||||
case MEOW_ITEM_WIDGET:
|
||||
meow_widget_draw(e->w);
|
||||
break;
|
||||
case MEOW_ITEM_CUSTOM:
|
||||
e->draw_cb(e->ctx);
|
||||
break;
|
||||
case MEOW_ITEM_VIEW: {
|
||||
meow_view *v = (meow_view *)e->ctx;
|
||||
if (v->hooks.on_draw) v->hooks.on_draw(v, meow_theme_get(), v->ctx);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void paint(int full, int x, int y, int w, int h)
|
||||
{
|
||||
uint32_t bg = meow_ui_page_bg();
|
||||
if (full) {
|
||||
meow_ui_clear(bg);
|
||||
} else {
|
||||
if (w <= 0 || h <= 0) return;
|
||||
meow_ui_fill_rect(x, y, w, h, bg);
|
||||
}
|
||||
/* 刷新 auto z + 组装按 (z,seq) 升序的绘制序(owner==active 过滤 WIDGET;view 须已进入)。 */
|
||||
draw_entry *order[MEOW_DRAW_MAX];
|
||||
int nn = 0;
|
||||
int active = meow_ui_activity_active();
|
||||
for (int i = 0; i < g_n; i++) {
|
||||
draw_entry *e = &g_e[i];
|
||||
e_compute_auto(e);
|
||||
if (e->kind == MEOW_ITEM_WIDGET && e->w && meow_widget_owner(e->w) != active) continue;
|
||||
if (e->kind == MEOW_ITEM_VIEW && !meow_draw_view_entered((meow_view *)e->ctx)) continue;
|
||||
order[nn++] = e;
|
||||
}
|
||||
/* 稳定插入排序:(z 升序,同 z 按 seq 升序=注册先序)。 */
|
||||
for (int i = 1; i < nn; i++) {
|
||||
draw_entry *key = order[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && (order[j]->z > key->z ||
|
||||
(order[j]->z == key->z && order[j]->seq > key->seq))) {
|
||||
order[j + 1] = order[j];
|
||||
j--;
|
||||
}
|
||||
order[j + 1] = key;
|
||||
}
|
||||
if (full) {
|
||||
for (int i = 0; i < nn; i++) draw_one(order[i]);
|
||||
meow_ui_flush();
|
||||
} else {
|
||||
int r[4];
|
||||
/* 区域重画(meow_ui_invalidate):把绘制 clip 收敛到本次失效矩形,只画「真正相交且可见」的
|
||||
* 部分——大绘制物(整页 card/布局)仅擦边相交时不再整块重画,而是裁到矩形内,避免 overdraw。
|
||||
* 控件内部还会再 push 自身矩形(嵌套求交 = 矩形∩控件,语义不变:越界仍被裁)。 */
|
||||
meow_gfx_clip_push(x, y, w, h);
|
||||
for (int i = 0; i < nn; i++) {
|
||||
if (!entry_rect(order[i], r)) continue;
|
||||
if (rect_hit(r[0], r[1], r[2], r[3], x, y, w, h)) draw_one(order[i]);
|
||||
}
|
||||
meow_gfx_clip_pop();
|
||||
meow_ui_flush_region(x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
void meow_ui_invalidate(int x, int y, int w, int h)
|
||||
{
|
||||
if (w <= 0 || h <= 0) return;
|
||||
paint(0, x, y, w, h);
|
||||
}
|
||||
|
||||
void meow_ui_paint_full(void)
|
||||
{
|
||||
paint(1, 0, 0, 0, 0);
|
||||
}
|
||||
+1252
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* meow_ui_global.c — App 跨 Activity 控件(§6.1 ②,SDK 托管骨架)。
|
||||
*
|
||||
* SDK 维护一个全局控件列表(钉在 App 的 Activity 栈之上、跨 Activity 常驻):
|
||||
* - 渲染:meow_ui_gfx present() 每帧在 active activity 内容之上、全局状态栏之下调
|
||||
* meow_ui_global_render()(遍历列表调 render_cb);
|
||||
* - 触摸:meow_ui_input TAP 分发先给 meow_ui_global_hit(x,y)——命中返回 1=消费,
|
||||
* 否则落到 active activity 页面控件(①)。
|
||||
*
|
||||
* 骨架(待 B2 细化):无 z 序细分/无按压态管理/无命中区排布——App 的 render/hit 回调
|
||||
* 自行决定绘制与命中。跨 Activity 常驻由「列表不随 Activity 栈变化」天然保证。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
#define MEOW_GLOBAL_MAX 16
|
||||
|
||||
typedef struct {
|
||||
int id;
|
||||
meow_global_render_fn render;
|
||||
meow_global_hit_fn hit;
|
||||
void *ctx;
|
||||
} gentry;
|
||||
|
||||
static gentry g_globals[MEOW_GLOBAL_MAX];
|
||||
static int g_global_n = 0;
|
||||
|
||||
void meow_ui_global_add(int id, meow_global_render_fn r, meow_global_hit_fn h, void *ctx)
|
||||
{
|
||||
for (int i = 0; i < g_global_n; i++)
|
||||
if (g_globals[i].id == id) return; /* 同 id 幂等(重复 add 不新增,骨架) */
|
||||
if (g_global_n >= MEOW_GLOBAL_MAX) return;
|
||||
g_globals[g_global_n].id = id;
|
||||
g_globals[g_global_n].render = r;
|
||||
g_globals[g_global_n].hit = h;
|
||||
g_globals[g_global_n].ctx = ctx;
|
||||
g_global_n++;
|
||||
}
|
||||
|
||||
void meow_ui_global_remove(int id)
|
||||
{
|
||||
for (int i = 0; i < g_global_n; i++) {
|
||||
if (g_globals[i].id == id) {
|
||||
g_globals[i] = g_globals[--g_global_n];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void meow_ui_global_render(void)
|
||||
{
|
||||
for (int i = 0; i < g_global_n; i++)
|
||||
if (g_globals[i].render) g_globals[i].render(g_globals[i].ctx);
|
||||
}
|
||||
|
||||
int meow_ui_global_hit(int x, int y)
|
||||
{
|
||||
for (int i = 0; i < g_global_n; i++)
|
||||
if (g_globals[i].hit && g_globals[i].hit(g_globals[i].ctx, x, y))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
/*
|
||||
* meow_ui_ime.c — 内置触屏软键盘(B1.6)。仅英文 QWERTY / 数字,ASCII。
|
||||
*
|
||||
* 阻塞:meow_ui_text_input(ctx) 弹出键盘,自绘自分发直到关闭(ESC / 点背景取消)。
|
||||
* 回车(K_ENTER)只提交(mark g_ime_result=1)不关闭,用户可继续输入。
|
||||
* 阻塞期间周期性喂 App on_tick(meow_ui_feed_tick),动态内容(进度条/时钟)不卡死。
|
||||
* 非阻塞:meow_ui_text_input_async(ctx) 打开后返回;meow_ui_run 在 on_tick 间隙
|
||||
* 随帧绘制并拦截触摸,app 用 meow_ime_active()/meow_ime_text() 轮询。
|
||||
*
|
||||
* 键盘覆盖屏幕底部约 300px;点按输入;键位 44×36(touch target 达标)。
|
||||
* M2.71-IME-fix:字母键面存小写("qwertyuiop"…),shift 开→大写、关→小写;
|
||||
* 数字/符号键不随 shift 转换。旧版键面存大写 → 无论 shift 开关恒输出大写(bug)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_pinyin.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* 阻塞 IME 循环周期性喂 App on_tick(实现在 meow_ui_input.c,读该 TU 的 g_events)。
|
||||
* 让 IME 打开期间 App 的动态内容(进度条/时钟)继续刷新;调用在 meow_ime_draw 之前,
|
||||
* 保证 App 重绘先进 back_buf、IME 面板随后盖上去(IME 始终最上,不被 App 重绘覆盖)。 */
|
||||
extern void meow_ui_feed_tick(void);
|
||||
|
||||
#define IME_MAXKEYS 96
|
||||
#define KB_TOP 420 /* 面板顶(预览行起点) */
|
||||
#define GW 44 /* 键宽 */
|
||||
#define GH 36 /* 键高 */
|
||||
#define GAP 4 /* 键间距 */
|
||||
#define GCOL 48 /* 列步进 = GW+GAP */
|
||||
|
||||
/* M2.71-bug7:键盘区背景快照/恢复。旧实现关闭后键盘像素留在屏底(残影)。此处打开前拍下
|
||||
* 键盘区 [KB_TOP,H),关闭后回填 + 上屏 → 无残影。 */
|
||||
static unsigned char *g_ime_cap = NULL;
|
||||
static int g_ime_cap_line = 0, g_ime_cap_h = 0;
|
||||
|
||||
static void ime_capture(void)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
int line = meow_ui_back_buf_pitch();
|
||||
int h = meow_ui_height() - KB_TOP;
|
||||
if (!bb || line <= 0 || h <= 0) { g_ime_cap = NULL; return; }
|
||||
size_t n = (size_t)line * (size_t)h;
|
||||
if (!g_ime_cap || g_ime_cap_line != line || g_ime_cap_h != h) {
|
||||
free(g_ime_cap);
|
||||
g_ime_cap = (unsigned char *)malloc(n);
|
||||
g_ime_cap_line = line; g_ime_cap_h = h;
|
||||
}
|
||||
if (g_ime_cap) memcpy(g_ime_cap, bb + (size_t)line * KB_TOP, n);
|
||||
}
|
||||
|
||||
static void ime_restore(void)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
if (!g_ime_cap || !bb) return;
|
||||
size_t n = (size_t)g_ime_cap_line * (size_t)g_ime_cap_h;
|
||||
memcpy(bb + (size_t)g_ime_cap_line * KB_TOP, g_ime_cap, n);
|
||||
meow_gfx_mark_dirty(0, KB_TOP, meow_ui_width(), g_ime_cap_h);
|
||||
meow_ui_flush();
|
||||
}
|
||||
|
||||
typedef enum { K_CH, K_SHIFT, K_SPACE, K_BACK, K_ENTER, K_ESC,
|
||||
K_PY, K_SWITCH, K_SYMBOL, K_COMMIT, K_CLEAR } kkind;
|
||||
|
||||
struct kbkey { int x, y, w, h; kkind kind; char ch; };
|
||||
|
||||
static struct kbkey g_keys[IME_MAXKEYS];
|
||||
static int g_nkeys = 0;
|
||||
|
||||
static int g_ime_active = 0;
|
||||
static int g_ime_done = 0;
|
||||
static int g_ime_result = 0;
|
||||
static int g_shift = 0;
|
||||
|
||||
static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */
|
||||
static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */
|
||||
static char g_ime_buf[256];
|
||||
static int g_maxlen = 255;
|
||||
|
||||
/* ---- 拼音模式(MEOW_IME_PINYIN)几何 + 状态(设计稿 §4.1/§5.2)----
|
||||
* 面板纵向分割:组合条 420..456、候选栏 456..504、键区 504..684(4 行×48,下缘留 36px)。
|
||||
* 组合串 g_py_buf 是 IME 内部态,不进字段(字段只含已上屏文本);候选每页 ≤6。 */
|
||||
#define PY_COMBO_Y (KB_TOP) /* 420 — 组合条顶 */
|
||||
#define PY_COMBO_H 36 /* 组合条高 */
|
||||
#define PY_CAND_Y 456 /* 候选栏顶 */
|
||||
#define PY_CAND_H 48 /* 候选栏高 */
|
||||
#define PY_KEY_TOP 504 /* 键区首行顶 */
|
||||
#define PY_PAGE 6 /* 每页候选数 */
|
||||
#define PY_MAXCAND 32 /* py_predict 候选上限(g_py_all 容量) */
|
||||
#define PY_PREV_X 6
|
||||
#define PY_PREV_W 34
|
||||
#define PY_NEXT_X 440
|
||||
#define PY_NEXT_W 34
|
||||
#define PY_CAND_X0 44 /* 候选起始 x */
|
||||
#define PY_CAND_GAP 14 /* 候选间距 */
|
||||
#define PY_CLEAR_X 388 /* 组合条「清」按钮 */
|
||||
#define PY_CLEAR_W 34
|
||||
#define PY_BADGE_X 426 /* 组合条「拼音」模式徽标 */
|
||||
#define PY_BADGE_W 46
|
||||
#define PY_BADGE_H 28
|
||||
#define PY_BADGE_Y (PY_COMBO_Y + 4)
|
||||
|
||||
static char g_py_buf[64]; /* 拼音组合串(去声调小写,≤8 音节) */
|
||||
static char g_py_all[PY_MAXCAND][64];/* 当前组合全部候选(UTF-8) */
|
||||
static int g_py_nc; /* 候选总数 */
|
||||
static int g_py_page; /* 翻页偏移(每页 ≤6) */
|
||||
static int g_py_active; /* 组合态标志 */
|
||||
static int g_ime_swipe_y = -1; /* 最近按下 Y(候选栏滑动翻页判定) */
|
||||
|
||||
/* ASCII/NUM 顶条右侧「中」切换热区(不占键位,英文零回归;GEOM 与 meow_ime_draw / handle_tap 共用)。 */
|
||||
#define ASCII_MID_X 430
|
||||
#define ASCII_MID_W 44
|
||||
|
||||
/* ---- 布局 ---- */
|
||||
static void add_key(int x, int y, int w, int h, kkind k, char ch)
|
||||
{
|
||||
if (g_nkeys >= IME_MAXKEYS) return;
|
||||
g_keys[g_nkeys].x = x; g_keys[g_nkeys].y = y;
|
||||
g_keys[g_nkeys].w = w; g_keys[g_nkeys].h = h;
|
||||
g_keys[g_nkeys].kind = k; g_keys[g_nkeys].ch = ch;
|
||||
g_nkeys++;
|
||||
}
|
||||
|
||||
/* 加入一行居中字符键;返回该行开始 x */
|
||||
static void add_ch_row(int y, const char *chars)
|
||||
{
|
||||
int n = (int)strlen(chars);
|
||||
int total = n * GW + (n - 1) * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y, GW, GH, K_CH, chars[i]);
|
||||
}
|
||||
|
||||
static void build_kb(meow_ime_mode mode)
|
||||
{
|
||||
g_nkeys = 0;
|
||||
int y = KB_TOP + 50;
|
||||
if (mode == MEOW_IME_PINYIN) {
|
||||
/* 4 行文本键(拼音只用到字母;数字/符号走 [?123] 切到既有 NUM 布局):
|
||||
* 行首 y=504,行 y=504/552/600/648,下缘 684。 */
|
||||
y = PY_KEY_TOP;
|
||||
add_ch_row(y, "qwertyuiop");
|
||||
/* 第 2 行:9 字母 + [拼/英](切 MEOW_IME_ASCII,复用 SHIFT 槽位) */
|
||||
{
|
||||
const char *r = "asdfghjkl";
|
||||
int n = (int)strlen(r);
|
||||
int total = 10 * GW + 9 * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL, GW, GH, K_CH, r[i]);
|
||||
add_key(x + 9 * GCOL, y + GCOL, GW, GH, K_SWITCH, '&');
|
||||
}
|
||||
/* 第 3 行:7 字母 + [←退格 3格] */
|
||||
{
|
||||
const char *r = "zxcvbnm";
|
||||
int n = (int)strlen(r);
|
||||
int total = 10 * GW + 9 * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL * 2, GW, GH, K_CH, r[i]);
|
||||
add_key(x + 7 * GCOL, y + GCOL * 2, (GW + GAP) * 3, GH, K_BACK, '<');
|
||||
}
|
||||
/* 第 4 行:[?123] [空格 6格] [OK](OK=只提交不关闭,同英文) */
|
||||
{
|
||||
int total = 10 * GW + 9 * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
add_key(x, y + GCOL * 3, GW, GH, K_SYMBOL, '1');
|
||||
add_key(x + GCOL, y + GCOL * 3, (GW + GAP) * 6, GH, K_SPACE, ' ');
|
||||
add_key(x + GCOL * 8, y + GCOL * 3, 2 * GW + GAP, GH, K_ENTER, '>');
|
||||
}
|
||||
} else if (mode == MEOW_IME_ASCII) {
|
||||
add_ch_row(y, "1234567890");
|
||||
add_ch_row(y + GCOL, "qwertyuiop");
|
||||
/* 第 3 行:9 字母 + SHIFT */
|
||||
{
|
||||
const char *r = "asdfghjkl";
|
||||
int n = (int)strlen(r);
|
||||
int total = 10 * GW + 9 * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL * 2, GW, GH, K_CH, r[i]);
|
||||
add_key(x + 9 * GCOL, y + GCOL * 2, GW, GH, K_SHIFT, '^');
|
||||
}
|
||||
/* 第 4 行:7 字母 + BACKSPACE */
|
||||
{
|
||||
const char *r = "zxcvbnm";
|
||||
int n = (int)strlen(r);
|
||||
int total = 10 * GW + 9 * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL * 3, GW, GH, K_CH, r[i]);
|
||||
add_key(x + 7 * GCOL, y + GCOL * 3, (GW + GAP) * 3, GH, K_BACK, '<');
|
||||
}
|
||||
/* 第 5 行:ESC + SPACE(宽) + ENTER */
|
||||
{
|
||||
int total = 10 * GW + 9 * GAP;
|
||||
int x = (480 - total) / 2;
|
||||
add_key(x, y + GCOL * 4, GW, GH, K_ESC, 'X');
|
||||
add_key(x + GCOL, y + GCOL * 4, (GW + GAP) * 6, GH, K_SPACE, ' ');
|
||||
add_key(x + GCOL * 8, y + GCOL * 4, 2 * GW + GAP, GH, K_ENTER, '>');
|
||||
}
|
||||
} else { /* MEOW_IME_NUM:居中九键数字盘(用户定案)——1-9 从上到下 3×3,0 垫底居中;
|
||||
* 顶行保留常用符号;底行 ESC(取消/关闭) | 0 | 退格+OK(只提交不关闭)。
|
||||
* M2.71-bug7:修掉旧版「同格重叠的 BACK+ENTER」与重复回车。
|
||||
* M2.71-IME-fix:回车不再关闭(与 ASCII 一致),关闭走 ESC/背景。 */
|
||||
add_ch_row(y, "-/.:*#+="); /* 符号行(8 键居中) */
|
||||
{
|
||||
/* 九键:3 列 × 100px 宽、列距 20,整体 340 宽居中(x0=70,190,310)。 */
|
||||
const int NW = 100, NG = 20;
|
||||
const int x0 = (480 - (3 * NW + 2 * NG)) / 2; /* = 70 */
|
||||
static const char pad[3][4] = { "123", "456", "789" };
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
add_key(x0 + c * (NW + NG), y + GCOL * (1 + r), NW, GH, K_CH, pad[r][c]);
|
||||
/* 底行:ESC(取消/关闭) | 0 | BACK + OK(只提交不关闭)。BACK/OK 各半格。 */
|
||||
int yb = y + GCOL * 4;
|
||||
add_key(x0, yb, NW, GH, K_ESC, 'X');
|
||||
add_key(x0 + (NW + NG), yb, NW, GH, K_CH, '0');
|
||||
int x3 = x0 + 2 * (NW + NG); /* 第 3 列左缘 */
|
||||
add_key(x3, yb, (NW - GAP) / 2, GH, K_BACK, '<');
|
||||
add_key(x3 + (NW - GAP) / 2 + GAP, yb, (NW - GAP) / 2, GH, K_ENTER, '>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 编辑动作 ---- */
|
||||
static int is_letter(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); }
|
||||
|
||||
/* ---- 拼音编辑动作(MEOW_IME_PINYIN) ---- */
|
||||
static int py_pages(void) { return (g_py_nc + PY_PAGE - 1) / PY_PAGE; }
|
||||
|
||||
static void py_refresh(void)
|
||||
{
|
||||
g_py_nc = py_predict(g_py_buf, g_py_all, PY_MAXCAND);
|
||||
g_py_page = 0;
|
||||
g_py_active = (g_py_buf[0] != 0);
|
||||
}
|
||||
|
||||
static void py_input(char c)
|
||||
{
|
||||
int len = (int)strlen(g_py_buf);
|
||||
if (len < (int)sizeof(g_py_buf) - 1) { g_py_buf[len] = c; g_py_buf[len + 1] = 0; }
|
||||
py_refresh();
|
||||
}
|
||||
|
||||
static void py_back_combo(void)
|
||||
{
|
||||
int len = (int)strlen(g_py_buf);
|
||||
if (len > 0) g_py_buf[len - 1] = 0;
|
||||
py_refresh();
|
||||
}
|
||||
|
||||
static void py_clear(void)
|
||||
{
|
||||
g_py_buf[0] = 0;
|
||||
py_refresh();
|
||||
g_ime_swipe_y = -1;
|
||||
}
|
||||
|
||||
/* 上屏词条到字段 + on_change + 清组合。maxlen 为字节预算(含 NUL)。 */
|
||||
static void py_commit(const char *s)
|
||||
{
|
||||
int len = (int)strlen(g_ime_buf);
|
||||
int sl = (s && *s) ? (int)strlen(s) : 0;
|
||||
if (sl > 0 && len + sl < g_maxlen) {
|
||||
memcpy(g_ime_buf + len, s, (size_t)sl + 1);
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
|
||||
}
|
||||
py_clear();
|
||||
}
|
||||
|
||||
/* UTF-8 安全退格:从字段末回退一个 codepoint(1-4 字节) */
|
||||
static void py_back_field(void)
|
||||
{
|
||||
int len = (int)strlen(g_ime_buf);
|
||||
if (len <= 0) return;
|
||||
int i = len - 1;
|
||||
while (i > 0 && ((unsigned char)g_ime_buf[i] & 0xC0) == 0x80) i--; /* 回退过续字节,找到起始字节 */
|
||||
g_ime_buf[i] = 0;
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
|
||||
}
|
||||
|
||||
static void py_page_next(void) { int p = py_pages(); if (g_py_page < p - 1) g_py_page++; }
|
||||
static void py_page_prev(void) { if (g_py_page > 0) g_py_page--; }
|
||||
static void py_commit_first(void) { if (g_py_nc > 0) py_commit(g_py_all[0]); }
|
||||
|
||||
static void py_set_mode(meow_ime_mode m)
|
||||
{
|
||||
g_ime_ctx.mode = m;
|
||||
py_clear(); /* 组合态未上屏内容丢弃(设计稿 §5.3:关闭/切换同语义) */
|
||||
g_shift = 0;
|
||||
build_kb(m);
|
||||
}
|
||||
|
||||
static void apply_key_py(const struct kbkey *k)
|
||||
{
|
||||
switch (k->kind) {
|
||||
case K_CH: py_input((char)tolower((unsigned char)k->ch)); break; /* 仅 a-z 字母进组合 */
|
||||
case K_BACK:
|
||||
if (g_py_buf[0]) py_back_combo(); /* 组合非空:删组合末字母 */
|
||||
else py_back_field(); /* 组合空:删字段末汉字(UTF-8 安全) */
|
||||
break;
|
||||
case K_SPACE:
|
||||
if (g_py_buf[0]) py_commit_first(); /* 组合非空:上屏首候选 */
|
||||
else { /* 空:上屏一个空格 */
|
||||
int len = (int)strlen(g_ime_buf);
|
||||
if (len < g_maxlen - 1) { g_ime_buf[len] = ' '; g_ime_buf[len + 1] = 0;
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx); }
|
||||
}
|
||||
break;
|
||||
case K_ENTER:
|
||||
if (g_py_buf[0]) py_commit_first(); /* 上屏首候选 + 清组合(不关闭) */
|
||||
else g_ime_result = 1; /* 组合空:只提交不关闭(走 ESC/背景) */
|
||||
break;
|
||||
case K_SWITCH: py_set_mode(MEOW_IME_ASCII); break; /* 拼→英 */
|
||||
case K_SYMBOL: py_set_mode(MEOW_IME_NUM); break; /* →数字/符号 */
|
||||
case K_ESC: g_ime_result = 0; g_ime_done = 1; break;
|
||||
case K_COMMIT: py_commit_first(); break;
|
||||
case K_CLEAR: py_clear(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_key(const struct kbkey *k)
|
||||
{
|
||||
if (g_ime_ctx.mode == MEOW_IME_PINYIN) { apply_key_py(k); return; }
|
||||
switch (k->kind) {
|
||||
case K_CH: {
|
||||
/* M2.71-IME-fix:键面存小写(build_kb "qwertyuiop"…),shift 开→大写、关→小写;
|
||||
* 数字/符号(非字母)不转换。旧版存大写 + else 分支恒返回大写 → 大小写不区分。 */
|
||||
char c = k->ch;
|
||||
if (g_shift && is_letter(c)) c = (char)toupper((unsigned char)c);
|
||||
int len = (int)strlen(g_ime_buf);
|
||||
if (len < g_maxlen - 1) {
|
||||
g_ime_buf[len] = c; g_ime_buf[len + 1] = 0;
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case K_SHIFT: g_shift = !g_shift; break;
|
||||
case K_SPACE: {
|
||||
int len = (int)strlen(g_ime_buf);
|
||||
if (len < g_maxlen - 1) { g_ime_buf[len] = ' '; g_ime_buf[len + 1] = 0;
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx); }
|
||||
break;
|
||||
}
|
||||
case K_BACK: {
|
||||
int len = (int)strlen(g_ime_buf);
|
||||
if (len > 0) { g_ime_buf[len - 1] = 0;
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx); }
|
||||
break;
|
||||
}
|
||||
case K_ENTER: g_ime_result = 1; break; /* M2.71-IME-fix:回车只提交(mark),不关键盘;关闭走 ESC/背景 */
|
||||
case K_ESC: g_ime_result = 0; g_ime_done = 1; break;
|
||||
default: break; /* K_PY/K_SWITCH/K_SYMBOL/K_COMMIT/K_CLEAR 仅拼音布局出现,英文分支不触 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 绘制 ---- */
|
||||
|
||||
/* 拼音键面标签(K_CH=字母小写;其余为该键的内置文字)。 */
|
||||
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";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
/* 候选栏内第 idx(页内 0..5)候选的实际 x/宽;越界返回 0。绘制与命中共用,保证几何一致。 */
|
||||
static int py_cand_rect(int idx, int *x, int *w)
|
||||
{
|
||||
int gi = g_py_page * PY_PAGE + idx;
|
||||
if (gi >= g_py_nc) return 0;
|
||||
int xp = PY_CAND_X0;
|
||||
for (int i = 0; i < idx; i++) {
|
||||
int pgi = g_py_page * PY_PAGE + i;
|
||||
if (pgi >= g_py_nc) break;
|
||||
xp += meow_ui_text_width(g_py_all[pgi], 20) + PY_CAND_GAP;
|
||||
}
|
||||
*x = xp;
|
||||
*w = meow_ui_text_width(g_py_all[gi], 20);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void meow_ime_draw_py(const meow_theme *th)
|
||||
{
|
||||
const int th20 = meow_ui_text_height(20);
|
||||
/* 面板整体 + 组合条 + 候选栏 */
|
||||
meow_ui_fill_rect(0, KB_TOP, 480, 720 - KB_TOP, th->surfaceContainerHighest);
|
||||
meow_ui_fill_rect(0, PY_COMBO_Y, 480, PY_COMBO_H, th->surfaceContainerHigh);
|
||||
|
||||
/* 组合条:left=组合串实时显示(含 '_' 光标);right=「清」+ 模式徽标「拼音」 */
|
||||
char pybuf[72];
|
||||
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_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);
|
||||
|
||||
/* 候选栏:◀ ▶ 翻页 + 横排候选(高亮第 0 项) */
|
||||
meow_ui_fill_rect(0, PY_CAND_Y, 480, PY_CAND_H, th->surfaceContainerHighest);
|
||||
meow_gfx_fill_round(PY_PREV_X, PY_CAND_Y, PY_PREV_W, PY_CAND_H, 8, th->surfaceContainerLow);
|
||||
meow_ui_text_center(PY_PREV_X + PY_PREV_W / 2, PY_CAND_Y + (PY_CAND_H - th20) / 2, "◀", 20, th->onSurface);
|
||||
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);
|
||||
} else {
|
||||
int x = PY_CAND_X0;
|
||||
for (int i = 0; i < PY_PAGE; i++) {
|
||||
int gi = g_py_page * PY_PAGE + i;
|
||||
if (gi >= g_py_nc) break;
|
||||
const char *c = g_py_all[gi];
|
||||
int w = meow_ui_text_width(c, 20);
|
||||
int ty = PY_CAND_Y + (PY_CAND_H - th20) / 2;
|
||||
if (i == 0) {
|
||||
meow_gfx_fill_round(x - 6, PY_CAND_Y + 6, w + 12, PY_CAND_H - 12, 8, th->secondaryContainer);
|
||||
meow_ui_text(x, ty, c, 20, th->onSecondaryContainer);
|
||||
} else {
|
||||
meow_ui_text(x, ty, c, 20, th->onSurface);
|
||||
}
|
||||
x += w + PY_CAND_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
/* 4 行键 */
|
||||
for (int i = 0; i < g_nkeys; i++) {
|
||||
const struct kbkey *k = &g_keys[i];
|
||||
uint32_t bg, fg;
|
||||
if (k->kind == K_ENTER) { bg = th->primary; fg = th->onPrimary; }
|
||||
else if (k->kind == K_SWITCH){ bg = th->tertiaryContainer; fg = th->onTertiaryContainer; }
|
||||
else if (k->kind == K_CH) { bg = th->surfaceContainerLow; fg = th->onSurface; }
|
||||
else { bg = th->primaryContainer; fg = th->onPrimaryContainer; }
|
||||
meow_gfx_fill_round(k->x, k->y, k->w, k->h, 8, bg);
|
||||
char tmp[4];
|
||||
meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - th20) / 2, py_key_label(k, tmp), 20, fg);
|
||||
}
|
||||
}
|
||||
|
||||
void meow_ime_draw(void)
|
||||
{
|
||||
if (!g_ime_active) return;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
if (g_ime_ctx.mode == MEOW_IME_PINYIN) { meow_ime_draw_py(th); return; }
|
||||
/* M2.71-bug7:配色全走 meow_theme(深/浅主题自动切),不再硬编码黑底白键。 */
|
||||
meow_ui_fill_rect(0, KB_TOP, 480, 720 - KB_TOP, th->surfaceContainerHighest); /* 面板 */
|
||||
meow_ui_fill_rect(0, KB_TOP, 480, 42, th->surfaceContainerHigh); /* 回显条 */
|
||||
char preview[260]; /* g_ime_buf(≤255) + '_' + NUL → 需 ≥257,防截断告警 */
|
||||
/* 回显=用户已输入的字符(live echo,非候选/联想),随键入实时刷新。 */
|
||||
snprintf(preview, sizeof preview, "%s_", g_ime_buf);
|
||||
meow_ui_text(14, KB_TOP + 9, preview, th->font_body, th->onSurface);
|
||||
/* ASCII/NUM 顶条右侧「中」热区:切到拼音模式(英文键布局零改动,零回归)。 */
|
||||
{
|
||||
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);
|
||||
}
|
||||
/* 键 */
|
||||
for (int i = 0; i < g_nkeys; i++) {
|
||||
const struct kbkey *k = &g_keys[i];
|
||||
uint32_t bg, fg;
|
||||
if (k->kind == K_ENTER) { bg = th->primary; fg = th->onPrimary; } /* OK=提交 */
|
||||
else if (k->kind == K_CH) { bg = th->surfaceContainerLow; fg = th->onSurface; } /* 字符键 */
|
||||
else { bg = th->primaryContainer; fg = th->onPrimaryContainer; } /* 功能键(ESC/SHIFT/空格/退格) */
|
||||
meow_gfx_fill_round(k->x, k->y, k->w, k->h, 8, bg);
|
||||
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); }
|
||||
else { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, label, 20, fg); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 命中 ---- */
|
||||
|
||||
/* 找命中键(供拼音长按判定退格用)。 */
|
||||
static const struct kbkey *key_at(int x, int y)
|
||||
{
|
||||
for (int i = 0; i < g_nkeys; i++) {
|
||||
const struct kbkey *k = &g_keys[i];
|
||||
if (x >= k->x && x < k->x + k->w && y >= k->y && y < k->y + k->h) return k;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 拼音命中:候选栏/组合条/键区。y<KB_TOP=取消(组合不上屏)。 */
|
||||
static int meow_ime_handle_tap_py(int x, int y)
|
||||
{
|
||||
if (y < KB_TOP) { g_ime_result = 0; g_ime_done = 1; return 1; } /* 背景点=取消 */
|
||||
if (y >= PY_CAND_Y && y < PY_CAND_Y + PY_CAND_H) { /* 候选栏 */
|
||||
if (x >= PY_PREV_X && x < PY_PREV_X + PY_PREV_W) { py_page_prev(); return 1; }
|
||||
if (x >= PY_NEXT_X && x < PY_NEXT_X + PY_NEXT_W) { py_page_next(); return 1; }
|
||||
/* 点候选:命中即上屏(UTF-8),清组合 */
|
||||
for (int i = 0; i < PY_PAGE; i++) {
|
||||
int cx, cw;
|
||||
if (!py_cand_rect(i, &cx, &cw)) break;
|
||||
if (x >= cx - 6 && x < cx + cw + 6) { py_commit(g_py_all[g_py_page * PY_PAGE + i]); return 1; }
|
||||
}
|
||||
return 1; /* 候选栏空隙:消费但不做 */
|
||||
}
|
||||
if (y >= PY_COMBO_Y && y < PY_COMBO_Y + PY_COMBO_H) { /* 组合条 */
|
||||
if (x >= PY_CLEAR_X && x < PY_CLEAR_X + PY_CLEAR_W) { py_clear(); return 1; } /* 清组合 */
|
||||
if (x >= PY_BADGE_X && x < PY_BADGE_X + PY_BADGE_W) { py_set_mode(MEOW_IME_ASCII); return 1; } /* 拼→英 */
|
||||
return 1; /* 组合条其他:消费 */
|
||||
}
|
||||
const struct kbkey *k = key_at(x, y);
|
||||
if (k) { apply_key(k); return 1; }
|
||||
return 1; /* 键区/面板空隙:消费但不做 */
|
||||
}
|
||||
|
||||
int meow_ime_handle_tap(int x, int y)
|
||||
{
|
||||
if (!g_ime_active || g_ime_done) return 0;
|
||||
if (g_ime_ctx.mode == MEOW_IME_PINYIN) return meow_ime_handle_tap_py(x, y);
|
||||
/* ASCII/NUM:顶条(回显条 KB_TOP..KB_TOP+42)右端「中」热区 → 切拼音;
|
||||
* y<KB_TOP=上方背景=取消;顶条其它区域消费不取消(沿用既有行为)。 */
|
||||
if (y < KB_TOP + 42) {
|
||||
if (x >= ASCII_MID_X && x < ASCII_MID_X + ASCII_MID_W) { py_set_mode(MEOW_IME_PINYIN); return 1; }
|
||||
if (y < KB_TOP) { g_ime_result = 0; g_ime_done = 1; return 1; } /* 背景点=取消 */
|
||||
return 1; /* 顶条其它:消费但不做(不落键) */
|
||||
}
|
||||
for (int i = 0; i < g_nkeys; i++) {
|
||||
const struct kbkey *k = &g_keys[i];
|
||||
if (x >= k->x && x < k->x + k->w && y >= k->y && y < k->y + k->h) {
|
||||
apply_key(k);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 1; /* 面板空隙:消费但不做 */
|
||||
}
|
||||
|
||||
/* ---- 对外 ---- */
|
||||
int meow_ime_active(void) { return g_ime_active && !g_ime_done; }
|
||||
const char *meow_ime_text(void) { return g_ime_buf; }
|
||||
const char *meow_ime_composition(void) { return g_ime_ctx.mode == MEOW_IME_PINYIN ? g_py_buf : ""; }
|
||||
int meow_ime_mode_pinyin(void) { return g_ime_active && !g_ime_done && g_ime_ctx.mode == MEOW_IME_PINYIN; }
|
||||
|
||||
/* 候选栏横滑翻页:滑动起始于候选栏区(y∈[456,504))才消费;首/尾页钳位。 */
|
||||
int meow_ime_swipe(int dir, int dist)
|
||||
{
|
||||
(void)dist;
|
||||
if (!meow_ime_mode_pinyin()) return 0;
|
||||
if (g_ime_swipe_y < PY_CAND_Y || g_ime_swipe_y >= PY_CAND_Y + PY_CAND_H) return 0;
|
||||
if (dir == MEOW_SWIPE_LEFT) { py_page_next(); return 1; }
|
||||
if (dir == MEOW_SWIPE_RIGHT) { py_page_prev(); return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 拼音长按退格:长按落在「退格」键上 → 清空字段 + 组合(上屏 off 回调)。 */
|
||||
int meow_ime_long_press(int x, int y)
|
||||
{
|
||||
if (!meow_ime_mode_pinyin()) return 0;
|
||||
const struct kbkey *k = key_at(x, y);
|
||||
if (!k || k->kind != K_BACK) return 0;
|
||||
g_ime_buf[0] = 0;
|
||||
py_clear();
|
||||
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 统一事件入口:TAP/TOUCH_DOWN/SWIPE/LONG_PRESS(IME 打开时主循环/阻塞循环调用)。
|
||||
* ASCII/NUM:TAP 走既有 handle_tap,其余返回 0(沿用英文行为,零回归)。 */
|
||||
int meow_ime_handle_event(const meow_input_ev *e)
|
||||
{
|
||||
if (!g_ime_active || g_ime_done) return 0;
|
||||
switch (e->type) {
|
||||
case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b);
|
||||
case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */
|
||||
case MEOW_EV_SWIPE: return meow_ime_swipe(e->a, e->b);
|
||||
case MEOW_EV_LONG_PRESS: return meow_ime_long_press(e->a, e->b);
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void meow_ime_close(int result)
|
||||
{
|
||||
if (!g_ime_active) return;
|
||||
if (result && g_ime_orig) {
|
||||
strncpy(g_ime_orig->buf, g_ime_buf, sizeof g_ime_orig->buf - 1);
|
||||
g_ime_orig->buf[sizeof g_ime_orig->buf - 1] = 0;
|
||||
}
|
||||
g_ime_active = 0; g_ime_done = 0;
|
||||
ime_restore(); /* M2.71-bug7:关闭还原键盘区背景,无残影 */
|
||||
}
|
||||
|
||||
void meow_ui_text_input_async(meow_ime_ctx *ctx)
|
||||
{
|
||||
if (!ctx) return;
|
||||
g_ime_orig = ctx;
|
||||
g_ime_ctx = *ctx; /* 拷贝(on_change 用) */
|
||||
strncpy(g_ime_buf, ctx->buf, sizeof g_ime_buf - 1);
|
||||
g_ime_buf[sizeof g_ime_buf - 1] = 0;
|
||||
g_maxlen = ctx->maxlen > 0 ? ctx->maxlen : 255;
|
||||
g_shift = 0; g_ime_result = 0; g_ime_done = 0; g_ime_active = 1;
|
||||
/* 拼音状态复位(组合/候选/翻页/压点) */
|
||||
g_py_buf[0] = 0; g_py_nc = 0; g_py_page = 0; g_py_active = 0; g_ime_swipe_y = -1;
|
||||
build_kb(ctx->mode);
|
||||
ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */
|
||||
meow_input_start();
|
||||
meow_input_drain();
|
||||
}
|
||||
|
||||
int meow_ui_text_input(meow_ime_ctx *ctx)
|
||||
{
|
||||
if (!ctx) return 0;
|
||||
meow_ui_text_input_async(ctx);
|
||||
/* 阻塞循环:直到 done;on_change 由 handle_tap 触发 */
|
||||
while (meow_ime_active()) {
|
||||
/* flush 挂起:整个周期内(事件处理触发的 on_change 重绘 + App on_tick 重绘)
|
||||
* 只记脏不上屏;IME 画完后一次 flush 原子上屏——否则 on_tick 的 invalidate
|
||||
* 会先把进度条等区域刷上屏盖住键盘,IME 随后再盖回,同区一帧两刷→频闪。 */
|
||||
meow_gfx_flush_hold(1);
|
||||
meow_input_ev e;
|
||||
while (meow_input_pop(&e)) {
|
||||
meow_ui_activity_ping(); /* M2.71-bug5:IME 打字交互也重置息屏计时 */
|
||||
meow_ime_handle_event(&e); /* TAP/TOUCH_DOWN/SWIPE/LONG_PRESS 统一分发(拼音/英文) */
|
||||
}
|
||||
/* 拼音 IME 打开时喂长按轮询(长按退格清空字段);英文模式不喂(保持既有行为)。 */
|
||||
if (meow_ime_mode_pinyin()) meow_input_long_press_poll();
|
||||
/* M2.71-IME-fix:阻塞 IME 期间喂 App on_tick(动态内容继续刷,progress/时钟不卡死)。
|
||||
* 顺序要求:先喂 on_tick(App 重绘进 back_buf)→ 再 meow_ime_draw(IME 面板随后盖上去),
|
||||
* 保证 IME 面板覆盖在最上,不被 App 的 d_paint_region 覆盖。 */
|
||||
meow_ui_feed_tick();
|
||||
meow_gfx_flush_hold(0);
|
||||
meow_ime_draw();
|
||||
meow_ui_flush();
|
||||
usleep(16000);
|
||||
}
|
||||
int result = g_ime_result;
|
||||
meow_ime_close(result);
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入),
|
||||
* 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "stb_image.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
struct meow_img {
|
||||
int w, h;
|
||||
unsigned char *rgba; /* 4 通道 straight alpha */
|
||||
};
|
||||
|
||||
static meow_img *img_alloc(int w, int h, unsigned char *rgba)
|
||||
{
|
||||
if (w <= 0 || h <= 0 || !rgba) { if (rgba) stbi_image_free(rgba); return NULL; }
|
||||
meow_img *im = (meow_img *)malloc(sizeof *im);
|
||||
if (!im) { stbi_image_free(rgba); return NULL; }
|
||||
im->w = w; im->h = h; im->rgba = rgba;
|
||||
return im;
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_mem(const void *buf, int len)
|
||||
{
|
||||
if (!buf || len <= 0) return NULL;
|
||||
int w = 0, h = 0, c = 0;
|
||||
unsigned char *p = stbi_load_from_memory((const unsigned char *)buf, len, &w, &h, &c, 4);
|
||||
if (!p) return NULL;
|
||||
return img_alloc(w, h, p);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_file(const char *path)
|
||||
{
|
||||
if (!path) return NULL;
|
||||
int w = 0, h = 0, c = 0;
|
||||
unsigned char *p = stbi_load(path, &w, &h, &c, 4);
|
||||
if (!p) return NULL;
|
||||
return img_alloc(w, h, p);
|
||||
}
|
||||
|
||||
void meow_img_free(meow_img *im)
|
||||
{
|
||||
if (!im) return;
|
||||
if (im->rgba) stbi_image_free(im->rgba);
|
||||
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; }
|
||||
|
||||
/* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
|
||||
{
|
||||
if (!im || dw <= 0 || dh <= 0) return NULL;
|
||||
unsigned char *out = (unsigned char *)malloc((size_t)dw * dh * 4);
|
||||
if (!out) return NULL;
|
||||
float s = 1.0f;
|
||||
if (cover) {
|
||||
/* 真正铺满(cover):每目标像素对应 s 个源像素,s = min(W/dw, H/dh)——缩放后图像
|
||||
* 宽/高两维都不小于目标(可能外溢),居中裁方。居中裁剪偏移(源像素)= (W-dw*s)/2
|
||||
* 恒 ≥ 0。曾误作 `s = max(sx,sy)` 且采样 `(dx+ox)/s`(除以缩放比):把整块输出映射到
|
||||
* 源左上角 ~dw/s 像素的透明角——图标(透明角 logo)整块透明、不绘制,即
|
||||
* 「icon: ... OK 但渲染不出(只显首字母占位)」根因。正确:源索引 = dx*s + 裁剪偏移
|
||||
* (与下方非 cover 分支 `dx*im->w/dw` 同一乘向)。 */
|
||||
float sx = (float)im->w / dw, sy = (float)im->h / dh;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
memcpy(out + ((size_t)dy * dw + dx) * 4,
|
||||
im->rgba + ((size_t)syi * im->w + sxi) * 4, 4);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int dy = 0; dy < dh; dy++) {
|
||||
int syi = (int)((float)dy * im->h / dh);
|
||||
if (syi >= im->h) syi = im->h - 1;
|
||||
for (int dx = 0; dx < dw; dx++) {
|
||||
int sxi = (int)((float)dx * im->w / dw);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void meow_ui_img(int x, int y, meow_img *im)
|
||||
{
|
||||
if (!im) return;
|
||||
meow_gfx_blit_rgba(x, y, im->w, im->h, im->rgba, 255);
|
||||
}
|
||||
|
||||
void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *im)
|
||||
{
|
||||
if (!im) return;
|
||||
unsigned char *tmp = scale_buf(im, w, h, 0);
|
||||
if (!tmp) return;
|
||||
meow_gfx_blit_rgba(x, y, w, h, tmp, 255);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
void meow_ui_img_alpha(int x, int y, int w, int h, meow_img *im, int alpha)
|
||||
{
|
||||
if (!im) return;
|
||||
unsigned char *tmp = scale_buf(im, w, h, 0);
|
||||
if (!tmp) return;
|
||||
meow_gfx_blit_rgba(x, y, w, h, tmp, alpha);
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
void meow_ui_img_cover(int x, int y, int size, meow_img *im)
|
||||
{
|
||||
if (!im) return;
|
||||
unsigned char *tmp = scale_buf(im, size, size, 1);
|
||||
if (!tmp) return;
|
||||
meow_gfx_blit_rgba(x, y, size, size, tmp, 255);
|
||||
free(tmp);
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
/*
|
||||
* meow_ui_input.c — 输入层(B1.4)。
|
||||
*
|
||||
* 建立输入线程,读 /dev/input/event*(event1=触摸 goodix-ts、event0=物理键
|
||||
* md-gpio-keys、event2=ADC 键),翻译成语义事件(KEY/TAP/SWIPE)压入环形队列;
|
||||
* meow_ui_run / IME 循环弹出分发。应用不再自读 event*。
|
||||
*
|
||||
* 触摸:BTN_TOUCH 按下记录原点,抬起时位移 < 阈值 → TAP,否则按主导轴 → SWIPE。
|
||||
* 物理键:read() 事件流 EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.55/agent-217 起系统按键
|
||||
* 语义在 SDK 输入层消化:
|
||||
* 电源 116 → 维持现状(息屏/亮屏/长按菜单由内核/sys_server/watchdog),SDK 不动、不下发 APP;
|
||||
* 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,volume_step/volume_max
|
||||
* 约束,与 launcher 设置页/watchdog 同源);
|
||||
* 播放/暂停 164、上一曲 165/19、下一曲 163 → 转发到当前媒体会话(App 是媒体源→本地回调,
|
||||
* 否则经 IPC 发 launcher 转发)。
|
||||
* 「App 收到的是语义事件」:上述音量/媒体键在 SDK 消化后**仍下发 MEOW_EV_KEY**(on_key),但
|
||||
* App 不得把它们映射成自定义行为(APPS.md §3.4.1 语义不变)。
|
||||
* 真机链路兜底(本次修复):物理键的按下不一定进入 evdev 事件流(事件流可被 EVIOCGRAB 独占或
|
||||
* 驱动不投递;APPS.md §4.4 明确 launcher 用 EVIOCGKEY 状态轮询读键、「与 grab/投递无关」)。故
|
||||
* 输入线程除 read() 事件流外,每轮还对键设备做一次 EVIOCGKEY 状态位图轮询,把 0→1/1→0 边沿并入
|
||||
* 同一套系统键语义(poll_sys_keys_eviocgkey);事件流与位图双源经 sys_key_press/sys_key_release
|
||||
* 的「同码 120ms 窗口」去重,一次物理按下只触发一步系统语义 + 一次 MEOW_EV_KEY 下发。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
/* linux input 事件常量(本设备实测) */
|
||||
#define EV_SYN 0
|
||||
#define EV_KEY 1
|
||||
#define EV_ABS 3
|
||||
#define BTN_TOUCH 0x14a
|
||||
#define ABS_X 0x00
|
||||
#define ABS_Y 0x01
|
||||
#define ABS_MT_POSITION_X 0x35
|
||||
#define ABS_MT_POSITION_Y 0x36
|
||||
#ifndef EVIOCGKEY
|
||||
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)
|
||||
#endif
|
||||
#ifndef KEY_MAX
|
||||
#define KEY_MAX 0x2ff
|
||||
#endif
|
||||
|
||||
#define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */
|
||||
#define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */
|
||||
#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 重置息屏计时 */
|
||||
|
||||
/* ── 内部事件:系统级「下拉展开通知中心」(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;
|
||||
|
||||
/* 环形队列(互斥保护) */
|
||||
static meow_input_ev g_q[QUEUE_N];
|
||||
static volatile int g_qh = 0, g_qt = 0;
|
||||
static pthread_mutex_t g_qm = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
/* 事件循环状态 */
|
||||
static volatile int g_exit = 0;
|
||||
static meow_ui_events g_events;
|
||||
static int g_events_set = 0;
|
||||
static uint64_t last_tick_ms = 0;
|
||||
/* M2.22(C):启动 cooldown。应用刚进入时,用户点列表那几下(BTN_TOUCH 残留/抬手尾巴)
|
||||
* 可能被当作「左缘滑动」→ 立即返回。窗口期丢弃 SWIPE(尤其 LEFT),仍接受 TAP。 */
|
||||
static uint64_t startup_cooldown_until = 0;
|
||||
#define STARTUP_COOLDOWN_MS 500
|
||||
|
||||
static uint64_t now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */
|
||||
static const char *meow_settings_path(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_SETTINGS_FILE");
|
||||
return (e && e[0]) ? e : "/usr/data/meow/settings.conf";
|
||||
}
|
||||
|
||||
/* ---- 插孔消抖(M2.92,与 launcher/watchdog 255 同款)----
|
||||
* 插拔 3.5/4.4 耳机的电气瞬态会在音量键 ADC(event2)上产生幽灵 VOL_UP/DOWN 按下
|
||||
* (真机实证:插 4.4 同帧 ramp 爬满上限)。插孔状态变化后 800ms 内忽略音量键边沿。
|
||||
* 节流:500ms 才真读 /sys 一次(不在热路径每圈读文件)。 */
|
||||
static int g_jack_combo = -1; /* hs*2+bal 缓存,-1=未读 */
|
||||
static unsigned long long g_jack_changed_at = 0, g_jack_polled_at = 0;
|
||||
|
||||
static int meow_input_jack_debounce(void)
|
||||
{
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
if (now - g_jack_polled_at >= 500) {
|
||||
g_jack_polled_at = now;
|
||||
int hs = -1, bal = -1;
|
||||
char b[8];
|
||||
int fd = open("/sys/class/switch/headset/state", O_RDONLY);
|
||||
if (fd >= 0) { ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n > 0) { b[n] = 0; hs = atoi(b); } }
|
||||
fd = open("/sys/class/switch/balance/state", O_RDONLY);
|
||||
if (fd >= 0) { ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n > 0) { b[n] = 0; bal = atoi(b); } }
|
||||
if (hs >= 0 && bal >= 0) {
|
||||
int combo = hs * 2 + bal;
|
||||
if (g_jack_combo >= 0 && combo != g_jack_combo) g_jack_changed_at = now;
|
||||
g_jack_combo = combo;
|
||||
}
|
||||
}
|
||||
return g_jack_changed_at && (now - g_jack_changed_at) < 800;
|
||||
}
|
||||
|
||||
/* ---- §10.5 叠加让位:通知中心叠加会话期输入排空(播放中 App 不被 SIGSTOP 的配套)----
|
||||
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,ncm 播放线程同进程),
|
||||
* App 输入线程仍在读同一批 evdev 设备(每 fd 各收一份事件副本,与 launcher 互不剥夺)——若不
|
||||
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发)。
|
||||
* 故标记存在期:排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作
|
||||
* 叠加期由 launcher/watchdog 单侧承担)。200ms 节流缓存存在性(与 present 层同口径,别每圈 stat)。 */
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)----
|
||||
* 物理键是系统资源(APPS.md §3.4.1),语义全局统一。本函数在输入线程执行系统动作:
|
||||
* 电源 116 → 息屏/亮屏/长按菜单(内核/sys_server/watchdog),SDK 不动(维持现状);
|
||||
* 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,与 launcher
|
||||
* 设置页/watchdog 同源:volume_step/volume_max 约束),仅按下边沿(1)一步;
|
||||
* 播放/上次曲 164/165/19/163 → 转发到当前媒体会话(App 是媒体源→本地回调;否则 IPC→launcher)。
|
||||
* 仅处理按下边沿(val==1)触发系统动作;重复(2)/抬起(0)不重复执行。on_key 的下发由输入线程
|
||||
* 在调用本函数后另行 push(音量/媒体键仍通知 App,App 不得映射)。 */
|
||||
static void meow_input_handle_sys_key(int code, int val)
|
||||
{
|
||||
switch (code) {
|
||||
case MEOW_KEY_POWER: /* 电源:维持现状,SDK 不动 */
|
||||
break;
|
||||
case MEOW_KEY_VOLUMEDOWN:
|
||||
case MEOW_KEY_VOLUMEUP:
|
||||
if (val == 1 && !meow_input_jack_debounce()) { /* 插孔瞬态期忽略幽灵音量边沿 */
|
||||
int dir = (code == MEOW_KEY_VOLUMEUP) ? +1 : -1;
|
||||
int nv = meow_ui_vol_step(meow_settings_path(), dir); /* settings 语义+持久化 */
|
||||
meow_ui_vol_dac_write(nv); /* 写真实 CS43198 DAC */
|
||||
}
|
||||
break;
|
||||
case MEOW_KEY_PLAYPAUSE:
|
||||
case MEOW_KEY_PREVIOUS:
|
||||
case MEOW_KEY_NEXT:
|
||||
case 19: /* KEY_PREVIOUSSONG 旧码 */
|
||||
if (val == 1) meow_ui_media_sys_key(code);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void meow_input_drain(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_qm);
|
||||
g_qh = 0; g_qt = 0;
|
||||
pthread_mutex_unlock(&g_qm);
|
||||
}
|
||||
|
||||
static void push_ev(int type, int a, int b)
|
||||
{
|
||||
pthread_mutex_lock(&g_qm);
|
||||
int n = (g_qt + 1) % QUEUE_N;
|
||||
if (n == g_qh) { /* 满:丢弃 */
|
||||
pthread_mutex_unlock(&g_qm);
|
||||
return;
|
||||
}
|
||||
g_q[g_qt].type = (uint8_t)type; g_q[g_qt].a = a; g_q[g_qt].b = b;
|
||||
g_qt = n;
|
||||
pthread_mutex_unlock(&g_qm);
|
||||
}
|
||||
|
||||
int meow_input_pop(meow_input_ev *ev)
|
||||
{
|
||||
if (!ev) return 0;
|
||||
pthread_mutex_lock(&g_qm);
|
||||
if (g_qh == g_qt) { pthread_mutex_unlock(&g_qm); return 0; }
|
||||
*ev = g_q[g_qh];
|
||||
g_qh = (g_qh + 1) % QUEUE_N;
|
||||
pthread_mutex_unlock(&g_qm);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void meow_input_inject(int type, int a, int b)
|
||||
{
|
||||
push_ev(type, a, b);
|
||||
}
|
||||
|
||||
/* ---- 触摸状态机(M2.22 B:手势级干净起点 + 原点严格锁定,杜绝残留坐标污染)----
|
||||
* 背景:goodix 事件时序不保证 BTN_TOUCH 与 ABS 的先后;旧版在按下边沿用 t_cx/t_cy
|
||||
* 立即锁原点,而 t_cx/t_cy 是上一手势的残留 —— 上一次「中部 tap」留下的坐标被本次
|
||||
* 按下当原点(>EDGE_W),导致后续左缘侧滑不返回、进度条不出。且进程内不自愈。
|
||||
* 修法:每次抬手彻底清空全部轨迹状态;原点只在「本次按下之后」出现的首个有效 ABS
|
||||
* 锁定(touch_update,gated 于 t_down && !t_origin_valid && x>=0 && y>=0)。 */
|
||||
static int t_down = 0;
|
||||
static int t_press_valid = 0; /* BTN_TOUCH=1 已见(本次按下的 gate),等首个有效 ABS 才锁原点 */
|
||||
static int t_dx = 0, t_dy = 0; /* 按下原点(BTN_TOUCH 后首个有效 ABS 锁定) */
|
||||
static int t_cx = 0, t_cy = 0; /* 当前坐标(-1=本次尚未见到有效坐标) */
|
||||
static int t_origin_valid = 0; /* 原点是否已锁定 */
|
||||
static uint64_t t_down_ms = 0; /* 按下时刻(monotonic ms),供实时按住时长用 */
|
||||
/* M2.71-bug2/6:拖动过程事件上次上报坐标(坐标变化才入队 MEOW_EV_TOUCH_MOVE,防刷屏)。 */
|
||||
static int t_mv_x = -1, t_mv_y = -1;
|
||||
/* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */
|
||||
static int t_long_fired = 0;
|
||||
|
||||
/* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */
|
||||
static int g_long_press_ms = MEOW_LONG_PRESS_MS;
|
||||
/* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */
|
||||
static meow_long_press_fn g_long_press_cb = NULL;
|
||||
|
||||
/* 按下边沿:只标记按下,绝不用传入坐标立即锁原点(避免用上一手势残留坐标当原点)。
|
||||
* t_cx/t_cy 归 -1 —— 本次手势以「无坐标」起步,只接受随后到来的有效 ABS。 */
|
||||
static void touch_press(int x, int y)
|
||||
{
|
||||
(void)x; (void)y;
|
||||
t_down = 1;
|
||||
t_press_valid = 1;
|
||||
t_origin_valid = 0;
|
||||
t_cx = -1; t_cy = -1;
|
||||
t_dx = 0; t_dy = 0;
|
||||
t_down_ms = now_ms(); /* 记按下时刻:供实时按住时长/按住视觉 */
|
||||
t_mv_x = -1; t_mv_y = -1; /* M2.71:拖动过程起点未定 */
|
||||
t_long_fired = 0; /* §6.3:每次新按压复位长按触发标记 */
|
||||
}
|
||||
/* 由 ABS_X/ABS_Y 驱动:若本次按下且原点未锁且坐标有效 → 锁原点;再更新当前坐标。
|
||||
* M2.71-bug2/6:原点锁定即报 MEOW_EV_TOUCH_DOWN(拖动起点);随后坐标变化报 MEOW_EV_TOUCH_MOVE
|
||||
* (坐标变了才报,省队列)。这两类事件供 slider 拖动 / 列表&页面滚动跟手。 */
|
||||
static void touch_update(int x, int y)
|
||||
{
|
||||
if (t_down && !t_origin_valid && x >= 0 && y >= 0) {
|
||||
t_dx = x; t_dy = y; t_origin_valid = 1;
|
||||
t_mv_x = x; t_mv_y = y;
|
||||
push_ev(MEOW_EV_TOUCH_DOWN, x, y);
|
||||
}
|
||||
if (x >= 0) t_cx = x;
|
||||
if (y >= 0) t_cy = y;
|
||||
if (t_down && t_origin_valid && t_cx >= 0 && t_cy >= 0 &&
|
||||
(t_cx != t_mv_x || t_cy != t_mv_y)) {
|
||||
t_mv_x = t_cx; t_mv_y = t_cy;
|
||||
push_ev(MEOW_EV_TOUCH_MOVE, t_cx, t_cy);
|
||||
}
|
||||
}
|
||||
static void touch_release(void)
|
||||
{
|
||||
if (!t_down) return;
|
||||
t_down = 0; t_press_valid = 0;
|
||||
int was_long = t_long_fired; /* §6.3:本次按压是否已触发过长按(抬手不再产生点击/滑动) */
|
||||
t_long_fired = 0;
|
||||
/* 本次按下从未锁到有效坐标(幽灵触摸/坐标未就绪):丢弃,且清空全部轨迹。 */
|
||||
if (!t_origin_valid) { t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1; return; }
|
||||
t_origin_valid = 0;
|
||||
int mdx = t_cx - t_dx, mdy = t_cy - t_dy;
|
||||
int adx = mdx < 0 ? -mdx : mdx, ady = mdy < 0 ? -mdy : mdy;
|
||||
/* 长按已触发:本次手势由长按消费,抬手不再产生 TAP/SWIPE/NOTIF_PULL(抑制点击,§6.3)。 */
|
||||
if (was_long) {
|
||||
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
|
||||
return;
|
||||
}
|
||||
/* 手势判定只在「抬手(release)边沿」做:按下/移动过程不触发 SWIPE,仅报 PROGRESS。
|
||||
*
|
||||
* 返回手势("左滑"语义):原点确实在左缘 + 向右移动 ≥SWIPE_THRESH。设备实际为
|
||||
* 「左缘向内(右)划」≥65px(M2.21:90→65 用户实测更顺手;与 watchdog 兜底一致)。
|
||||
* 原点 t_dx 是本次按下后首个有效 ABS 锁定的真实按下点(非残留),且 t_cx/t_dx 已
|
||||
* 被手势级清空,绝不继承上一手势坐标,确保:
|
||||
* ① 中部右滑不会误判成左缘返回(t_dx>=EDGE_W);
|
||||
* ② 先点中部再侧滑不返回(原点锁定在中部,>=EDGE_W),符合「必须从左缘起划」;
|
||||
* ③ 右移严格正向(t_cx-t_dx>=0),杜绝「向左划也出条/误返回」。
|
||||
* SDK 统一上报为 MEOW_SWIPE_LEFT(设计"左滑=返回"契约,hello 用 on_swipe(LEFT) 退出)。
|
||||
*
|
||||
* 屏幕中部滑动不是返回手势:中部右滑→RIGHT(内容导航),中部左滑→非返回(落 TAP,
|
||||
* 避免任意处左滑误退出)。纵向滑动→UP/DOWN 照旧。 */
|
||||
if (adx >= SWIPE_THRESH || ady >= SWIPE_THRESH) {
|
||||
/* 顶部下滑 → 通知中心(系统保留手势,不下发 APP):起点在顶部热区 + 有真实向下位移。
|
||||
* 在主导轴判定(adx vs ady)之前先行识别并置最高优先级——顶起「起点同时落在顶部热区与
|
||||
* 左缘」的边界(定案:起点 y 在顶部热区 → 顶部手势优先)。若无此优先,下落时含右向分量的
|
||||
* 手势会被水平分量抢先判成 MEOW_SWIPE_LEFT → 漏给 App on_swipe → 根页退出(真机闪退链)。
|
||||
* 热区高度随状态变化(非全屏=整条状态栏、全屏=MEOW_SBAR_HOTZONE_FS 缩小、收合=提示线高)
|
||||
* ——全屏 app 只在顶部极窄热区被系统占用,故「不影响全屏 app」。
|
||||
* 仅当真「向下拉」(mdy>0 且 ady≥SWIPE_THRESH)才认:纯水平右滑(左缘返回)或上滑不误判。 */
|
||||
if (mdy > 0 && ady >= SWIPE_THRESH &&
|
||||
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 if (adx >= ady) {
|
||||
if (t_dx >= 0 && t_dx < EDGE_W && (t_cx - t_dx) >= SWIPE_THRESH) {
|
||||
push_ev(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, adx); /* 左缘返回手势 */
|
||||
} else if (mdx > 0) {
|
||||
push_ev(MEOW_EV_SWIPE, MEOW_SWIPE_RIGHT, adx); /* 中部右滑:内容 */
|
||||
} else {
|
||||
push_ev(MEOW_EV_TAP, t_cx, t_cy); /* 中部左滑:非返回 */
|
||||
}
|
||||
} else {
|
||||
int dir = mdy > 0 ? MEOW_SWIPE_DOWN : MEOW_SWIPE_UP;
|
||||
push_ev(MEOW_EV_SWIPE, dir, ady > adx ? ady : adx);
|
||||
}
|
||||
} else {
|
||||
push_ev(MEOW_EV_TAP, t_cx, t_cy);
|
||||
}
|
||||
/* 手势级干净起点:清空全部轨迹,下一手势从零开始,杜绝污染无自愈。 */
|
||||
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
|
||||
}
|
||||
|
||||
/* ---- 实时触摸态(内部:控件绘制按压/按住视觉用)----
|
||||
* 直接读触摸状态机当前值(与语义事件队列无关;无按下/坐标未就绪时 x/y 返回 -1)。 */
|
||||
int meow_input_touch_down(void) { return t_down; }
|
||||
int meow_input_touch_x(void) { return t_cx; }
|
||||
int meow_input_touch_y(void) { return t_cy; }
|
||||
unsigned int meow_input_touch_hold_ms(void)
|
||||
{
|
||||
if (!t_down) return 0;
|
||||
uint64_t e = now_ms() - t_down_ms;
|
||||
if (e > 0xFFFFFFFFu) e = 0xFFFFFFFFu;
|
||||
return (unsigned int)e;
|
||||
}
|
||||
|
||||
/* 宿主单测/内部注入:模拟按压(down=1 时 x,y=压点、hold_ms=已按住时长),设备运行时无调用方。 */
|
||||
void meow_input_touch_set(int down, int x, int y, unsigned hold_ms)
|
||||
{
|
||||
if (down) {
|
||||
t_down = 1; t_press_valid = 1; t_origin_valid = 1; t_long_fired = 0;
|
||||
t_cx = x; t_cy = y; t_dx = x; t_dy = y;
|
||||
uint64_t now = now_ms();
|
||||
t_down_ms = (now > (uint64_t)hold_ms) ? (now - (uint64_t)hold_ms) : 0;
|
||||
} else {
|
||||
t_down = 0; t_press_valid = 0; t_origin_valid = 0; t_long_fired = 0;
|
||||
t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = 0;
|
||||
t_mv_x = -1; t_mv_y = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 宿主单测/内部注入:模拟按下后移动到 (x,y)(保原点、更新当前坐标;位移超长按容忍即取消)。
|
||||
* 对应输入线程 touch_update 的位移分支(这里不产生语义事件,仅更新坐标状态)。 */
|
||||
void meow_input_touch_move(int x, int y)
|
||||
{
|
||||
if (!t_down || !t_origin_valid) return;
|
||||
t_cx = x; t_cy = y;
|
||||
t_mv_x = x; t_mv_y = y;
|
||||
}
|
||||
|
||||
/* 宿主单测/内部注入:模拟一次抬手(真实 touch_release;含长按后抑制 TAP/SWIPE)。 */
|
||||
void meow_input_touch_release(void) { touch_release(); }
|
||||
|
||||
/* ---- §6.3 通用长按公共 API + 阈值跨越检测 ----
|
||||
* meow_input_long_press_poll:主循环(meow_ui_run 每帧)与宿主单测调用。读触摸状态机:
|
||||
* 按下、原点/坐标已就绪、未触发过长按、位移 ≤ 容忍、按住时长 ≥ 阈值 → 置 t_long_fired,
|
||||
* 入队 MEOW_EV_LONG_PRESS(a=x,b=y=当前按下点),返回 1;否则返回 0。幂等(一次按压一次)。 */
|
||||
void meow_ui_set_long_press_cb(meow_long_press_fn cb) { g_long_press_cb = cb; }
|
||||
void meow_ui_set_long_press_ms(int ms)
|
||||
{ g_long_press_ms = (ms > 0) ? ms : (int)MEOW_LONG_PRESS_MS; }
|
||||
int meow_ui_long_press_ms(void) { return g_long_press_ms; }
|
||||
|
||||
int meow_input_long_press_poll(void)
|
||||
{
|
||||
if (!g_long_press_cb) return 0; /* 未注册回调:长按整体关闭,保持旧点击语义(短按/长按都 TAP) */
|
||||
if (!t_down || t_long_fired || !t_origin_valid || t_cx < 0 || t_cy < 0) return 0;
|
||||
int mdx = t_cx - t_dx, mdy = t_cy - t_dy;
|
||||
int amx = mdx < 0 ? -mdx : mdx, amy = mdy < 0 ? -mdy : mdy;
|
||||
int move = amx > amy ? amx : amy;
|
||||
if (move > MEOW_LONG_PRESS_SLOP) return 0; /* 位移超容忍:取消 */
|
||||
if (meow_input_touch_hold_ms() < (unsigned)g_long_press_ms) return 0; /* 未达阈值 */
|
||||
t_long_fired = 1;
|
||||
push_ev(MEOW_EV_LONG_PRESS, t_cx, t_cy);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- 系统键(音量/媒体)去重 + EVIOCGKEY 状态轮询 ----
|
||||
* M2.55/agent-217 之后 SDK 在输入线程消化系统键语义;但真机物理键(jz adc keyboard event2、
|
||||
* md-gpio-keys event0、earpods_adc event3)的按下不一定进入 evdev 事件流(事件流可被其它进程
|
||||
* EVIOCGRAB 独占或驱动不投递),而 launcher 全程用 EVIOCGKEY 状态轮询读键(APPS.md §4.4,
|
||||
* 「与 grab/投递无关」)。为让 App 前台物理音量/媒体键可靠生效,输入线程除 read() 事件流外,
|
||||
* 还按固定节奏轮询内核按键位图(EVIOCGKEY),把 0→1/1→0 边沿并入同一套系统键语义。
|
||||
* 双源(事件流 + 状态位图)对同一次物理按下各报一次,用「同码 120ms 窗口」去重,保证
|
||||
* 一次按下只触发一步系统语义 + 一次 MEOW_EV_KEY 下发(App on_key 收到语义事件,不得映射)。 */
|
||||
static int g_sys_press_code = -1;
|
||||
static uint64_t g_sys_press_ms = 0;
|
||||
#define SYS_KEY_DEDUP_MS 120
|
||||
|
||||
static void sys_key_press(int code)
|
||||
{
|
||||
uint64_t now = now_ms();
|
||||
if (code == g_sys_press_code && now - g_sys_press_ms < SYS_KEY_DEDUP_MS)
|
||||
return; /* 事件流 + EVIOCGKEY 双源去重:同码窗口内只消化一次 */
|
||||
g_sys_press_code = code;
|
||||
g_sys_press_ms = now;
|
||||
meow_input_handle_sys_key(code, 1);
|
||||
push_ev(MEOW_EV_KEY, code, 1);
|
||||
}
|
||||
|
||||
static void sys_key_release(int code)
|
||||
{
|
||||
push_ev(MEOW_EV_KEY, code, 0);
|
||||
}
|
||||
|
||||
/* 对一个 event 设备做一次 EVIOCGKEY 状态边沿检测(prev 为该设备上一状态位图,原地更新)。
|
||||
* 只在系统键码(音量/媒体)上找边沿;电源(116)不进此表——SDK 维持「不动、不下发」纪律。 */
|
||||
static void poll_sys_keys_eviocgkey(int fd, unsigned char *prev)
|
||||
{
|
||||
unsigned char cur[KEY_MAX / 8 + 1];
|
||||
if (ioctl(fd, EVIOCGKEY(sizeof cur), cur) < 0) return;
|
||||
static const int sys_codes[] = {
|
||||
MEOW_KEY_VOLUMEDOWN, MEOW_KEY_VOLUMEUP, MEOW_KEY_PLAYPAUSE,
|
||||
MEOW_KEY_PREVIOUS, MEOW_KEY_NEXT, 19,
|
||||
};
|
||||
for (unsigned ci = 0; ci < sizeof sys_codes / sizeof sys_codes[0]; ci++) {
|
||||
int code = sys_codes[ci];
|
||||
int b = (cur[code / 8] >> (code % 8)) & 1;
|
||||
int pb = (prev[code / 8] >> (code % 8)) & 1;
|
||||
if (b == pb) continue;
|
||||
prev[code / 8] = (unsigned char)(b ? (prev[code / 8] | (1u << (code % 8)))
|
||||
: (prev[code / 8] & (unsigned char)~(1u << (code % 8))));
|
||||
if (b) sys_key_press(code); /* 0→1:按下(去重后触发一步系统语义) */
|
||||
else sys_key_release(code); /* 1→0:抬起 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 输入线程:poll 所有 event 设备 + EVIOCGKEY 系统键轮询 ---- */
|
||||
static void *input_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
struct pollfd fds[MAX_DEVS];
|
||||
unsigned char prev_key[MAX_DEVS][KEY_MAX / 8 + 1];
|
||||
int nfd = 0;
|
||||
char dev[32];
|
||||
for (int i = 0; i < MAX_DEVS; i++) {
|
||||
snprintf(dev, sizeof dev, "/dev/input/event%d", i);
|
||||
int e = open(dev, O_RDONLY | O_NONBLOCK);
|
||||
if (e >= 0) {
|
||||
fds[nfd].fd = e; fds[nfd].events = POLLIN; fds[nfd].revents = 0;
|
||||
/* EVIOCGKEY 上一状态初始化为当前按下位图:启动时已按住的键不误判为新按下 */
|
||||
memset(prev_key[nfd], 0, sizeof prev_key[nfd]);
|
||||
if (ioctl(e, EVIOCGKEY(sizeof prev_key[nfd]), prev_key[nfd]) < 0)
|
||||
memset(prev_key[nfd], 0, sizeof prev_key[nfd]);
|
||||
nfd++;
|
||||
}
|
||||
}
|
||||
|
||||
int overlay_was = 0; /* §10.5:上一圈是否处于叠加抑制(进入沿复位触摸状态机用) */
|
||||
while (g_thread_run) {
|
||||
if (nfd == 0) { usleep(20000); continue; }
|
||||
int r = poll(fds, nfd, 20);
|
||||
/* §10.5 叠加让位:通知中心叠加会话期(标记存在)排空并丢弃本进程事件副本——面板输入
|
||||
* 由 launcher 接管,App 不得再消费(防面板点击被 App 双重触发);EVIOCGKEY 位图静默
|
||||
* 对齐(不产生边沿语义/系统键动作,解除后不会把面板期按键误判为新边沿)。 */
|
||||
if (overlay_input_suppressed()) {
|
||||
if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */
|
||||
overlay_was = 1;
|
||||
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
|
||||
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
|
||||
t_long_fired = 0;
|
||||
}
|
||||
if (r > 0)
|
||||
for (int i = 0; i < nfd; i++) {
|
||||
if (!(fds[i].revents & POLLIN)) continue;
|
||||
unsigned char junk[64];
|
||||
while (read(fds[i].fd, junk, sizeof junk) > 0) { /* 整批丢弃 */ }
|
||||
}
|
||||
for (int i = 0; i < nfd; i++) {
|
||||
unsigned char cur[KEY_MAX / 8 + 1];
|
||||
if (ioctl(fds[i].fd, EVIOCGKEY(sizeof cur), cur) == 0)
|
||||
memcpy(prev_key[i], cur, sizeof cur);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
overlay_was = 0;
|
||||
if (r < 0) {
|
||||
if (errno != EINTR) usleep(20000);
|
||||
} else if (r > 0) {
|
||||
for (int i = 0; i < nfd; i++) {
|
||||
if (!(fds[i].revents & POLLIN)) continue;
|
||||
unsigned char ev[16];
|
||||
while (read(fds[i].fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||||
uint16_t type = *(uint16_t *)(ev + 8);
|
||||
uint16_t code = *(uint16_t *)(ev + 10);
|
||||
int32_t val = *(int32_t *)(ev + 12);
|
||||
if (type == EV_ABS) {
|
||||
if (code == ABS_X || code == ABS_MT_POSITION_X) {
|
||||
if (t_down) touch_update(val, t_cy); /* 锁原点+更新当前(t_down 时才驱动) */
|
||||
else t_cx = val;
|
||||
/* 左滑进度实时上报(视觉条):原点确实在左缘(t_dx<EDGE_W) 且 向右移动
|
||||
* (t_cx>t_dx)——grow/回缩全程由「非全屏已验证的同一条路径」驱动,全屏仅
|
||||
* 垂直位置 hy 不同(hy=meow_ui_statusbar_height()),别无全屏特判。
|
||||
* 抬手触发仍由 touch_release 判定;这里只报过程(事件载荷 b=当前X)。
|
||||
* t_dx 是本次按下后首个有效 ABS 锁定的真实按下点(非残留),且每次抬手已
|
||||
* 清空,杜绝「中部点一下出条 / 向左划也出条」。 */
|
||||
if (t_down && t_origin_valid && t_dx >= 0 && t_dx < EDGE_W && t_cx > t_dx)
|
||||
push_ev(MEOW_EV_SWIPE_PROGRESS, t_dx, t_cx);
|
||||
}
|
||||
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) {
|
||||
if (t_down) touch_update(t_cx, val); else t_cy = val;
|
||||
}
|
||||
} else if (type == EV_KEY) {
|
||||
if (code == BTN_TOUCH) {
|
||||
if (val && !t_down) touch_press(t_cx, t_cy);
|
||||
else if (!val && t_down) touch_release();
|
||||
} else if (code == MEOW_KEY_POWER) {
|
||||
/* M2.55(接 M2.24 纪律):电源=息屏/亮屏/长按菜单,由内核/sys_server/
|
||||
* watchdog 统一处理,SDK 不动、也不下发 APP。维持现状。 */
|
||||
} else if (code == MEOW_KEY_VOLUMEDOWN || code == MEOW_KEY_VOLUMEUP ||
|
||||
code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_PREVIOUS ||
|
||||
code == MEOW_KEY_NEXT || code == 19) {
|
||||
/* 系统键(音量/媒体):真实物理键可能不进事件流,另经 EVIOCGKEY 状态轮询
|
||||
* 兜底(poll_sys_keys_eviocgkey);双源经 sys_key_press/sys_key_release 去重,
|
||||
* 一次按下只一步系统语义 + 一次 MEOW_EV_KEY 下发(App on_key 不得映射)。 */
|
||||
if (val == 1) sys_key_press(code);
|
||||
else if (val == 0) sys_key_release(code);
|
||||
} else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */
|
||||
push_ev(MEOW_EV_KEY, code, 1);
|
||||
} else if (val == 0) { /* 抬起 */
|
||||
push_ev(MEOW_EV_KEY, code, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* EVIOCGKEY 系统键状态轮询:无论事件流是否投递都跑(真实物理键可能不进事件流,
|
||||
* 与 launcher kp_poll 同源的按键位图读取兜底;事件流 + 位图双源在 sys_key_press 去重)。 */
|
||||
for (int i = 0; i < nfd; i++)
|
||||
poll_sys_keys_eviocgkey(fds[i].fd, prev_key[i]);
|
||||
}
|
||||
for (int i = 0; i < nfd; i++) close(fds[i].fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void meow_input_start(void)
|
||||
{
|
||||
if (g_thread_started) return;
|
||||
g_thread_started = 1;
|
||||
g_thread_run = 1;
|
||||
pthread_create(&g_thread, NULL, input_thread, NULL);
|
||||
}
|
||||
|
||||
/* ---- 事件循环 ---- */
|
||||
void meow_ui_request_exit(void) { g_exit = 1; }
|
||||
|
||||
/* M2.71-bug5:App 活动信号(节流 ≤2/s 写标记文件,限 I/O)。
|
||||
* 仅真机写(宿主/qemu 无 launcher 消费,避免噪音);launcher 前台 app 循环消费它重置息屏计时。 */
|
||||
static uint64_t g_activity_last_ms = 0;
|
||||
void meow_ui_activity_ping(void)
|
||||
{
|
||||
uint64_t now = now_ms();
|
||||
if (now - g_activity_last_ms < 500) return; /* 节流:任意交互连续触摸也只 ~2 次/s */
|
||||
g_activity_last_ms = now;
|
||||
if (!meow_gfx_is_device()) return; /* 离屏/宿主:无 launcher 消费 */
|
||||
int fd = open(MEOW_APP_ACTIVITY_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) { if (write(fd, "1", 1) < 0) { } close(fd); }
|
||||
}
|
||||
|
||||
void meow_ui_run(const meow_ui_events *ev)
|
||||
{
|
||||
if (ev) { g_events = *ev; g_events_set = 1; }
|
||||
g_exit = 0;
|
||||
meow_input_start();
|
||||
last_tick_ms = now_ms();
|
||||
startup_cooldown_until = now_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */
|
||||
|
||||
while (!g_exit) {
|
||||
/* agent-119:实时触摸按压/按住/释放 → 即时重绘命中按钮区(不等 app 重绘)。
|
||||
* IME 打开时软键盘为覆盖层且拦截触摸,不驱动背后按钮视觉,跳过。
|
||||
* §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动;启动窗口不触发)。 */
|
||||
if (!meow_ime_active()) {
|
||||
meow_widget_touch_refresh();
|
||||
if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll();
|
||||
} else if (meow_ime_mode_pinyin()) {
|
||||
/* 拼音 IME 打开:喂长按轮询(长按退格清空字段)。英文模式不喂(保持既有行为)。 */
|
||||
if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll();
|
||||
}
|
||||
|
||||
meow_input_ev e;
|
||||
int got = 0;
|
||||
while (meow_input_pop(&e)) {
|
||||
if (g_exit) break; /* 已请求退出:不再分发剩余事件,杜绝「退出后又触发」的偶发再进/退 */
|
||||
got = 1;
|
||||
meow_ui_activity_ping(); /* M2.71-bug5:任意交互事件都喂 launcher 重置息屏计时(节流) */
|
||||
int in_startup = (now_ms() < startup_cooldown_until); /* M2.22(C) */
|
||||
if (e.type == MEOW_EV_TAP) {
|
||||
if (in_startup) continue; /* 启动窗口也吞 TAP:旧手势残留点按(如上一次左滑抬起)
|
||||
* 在进入瞬间命中 UI 按钮(如全屏按钮)会误触发——丢弃。 */
|
||||
/* 抬手未达标:清视觉条 */
|
||||
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.1 触摸分流:③ 系统全局(IME/状态栏…已在上面消费) → ② App 全局控件
|
||||
* (命中返回 1=消费,否则落到 ① active Activity 页面控件)。 */
|
||||
if (meow_ui_global_hit(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) {
|
||||
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; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */
|
||||
/* 抬手达标:清视觉条(左滑返回),触发行消失 */
|
||||
meow_ui_swipe_hint(-1);
|
||||
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;
|
||||
/* A-0 全局左缘返回(方案A):
|
||||
* - APP 注册 on_swipe → APP 接管,自行决定(meow_ui_request_exit / 页内返回);
|
||||
* - APP 未注册 on_swipe 且非全屏(sb_active)→ SDK 默认退出 app(返回=退出);
|
||||
* - 全屏/沉浸 → 无系统返回手势,仅 APP 自己处理。
|
||||
* 扩展点:阶段D M3 导航栈落地后,「默认退出」升级为「按 nav_stack 回退」。 */
|
||||
int sb_active = meow_ui_sys_statusbar_active();
|
||||
if (g_events_set && g_events.on_swipe) {
|
||||
g_events.on_swipe(e.a, e.b, 0);
|
||||
} 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;路由到系统钩子(App 进程只检测+经 IPC 上报 MSG_NOTIF_OPEN,
|
||||
* 通知中心由 launcher 渲染,见 FRAMEWORK_DESIGN §10.5)。
|
||||
* 起点若同时落在左缘,拖动期可能已画过 MEOW_EV_SWIPE_PROGRESS 左缘指示条,
|
||||
* 抬手定判为 NOTIF_PULL 后须清掉(否则通知中心打开后残留返回条)。 */
|
||||
meow_ui_swipe_hint(-1);
|
||||
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 弹出时触摸优先喂键盘(键盘覆盖层拦截,
|
||||
* on_touch_* 丢弃,避免按到键盘却在背后控件上锁拖拽);启动窗口也吞(防残留点按
|
||||
* 误锁拖拽)。模态(dialog/sheet)期间本主循环被阻塞,天然不在此分发。
|
||||
* IME 打开时先把压点喂给拼音(候选栏滑动翻页判定 g_ime_swipe_y),再丢弃。 */
|
||||
if (in_startup) continue;
|
||||
if (meow_ime_active()) { meow_ime_handle_event(&e); continue; }
|
||||
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)
|
||||
g_events.on_touch_move(e.a, e.b);
|
||||
got = 1; /* 交互事件:不睡眠,保持跟手帧同步 */
|
||||
} 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 */
|
||||
/* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。
|
||||
* 全屏/收合下「左缘返回手势保留」,顶部动态提示条(meow_ui_swipe_hint)照常绘制
|
||||
* (与 agent-108:不再以 meow_ui_sys_statusbar_active()(全屏/隐藏=0)开关,否则
|
||||
* 全屏时滑条完全不显示)。条位 y=meow_ui_statusbar_height():非全屏=状态栏下沿
|
||||
* (28)、全屏=顶部、收合=提示线下缘(MUI_SBAR_COLLAPSE_H),与提示线不重叠加——
|
||||
* 全屏与非全屏共用同一条绘制/进度路径,只 hy 不同,无全屏特有的坐标/方向/最大值逻辑。
|
||||
* APP 若注册 on_swipe_progress 仍可自绘(系统条照常画;x_start+dx==当前X,幂等)。 */
|
||||
meow_ui_swipe_hint(xc);
|
||||
if (g_events_set && g_events.on_swipe_progress)
|
||||
g_events.on_swipe_progress(x0, dx);
|
||||
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
|
||||
} else if (e.type == MEOW_EV_LONG_PRESS) {
|
||||
/* §6.3 通用长按:按下 ≥ 阈值未抬手触发。坐标=触发时刻按下点(App 用
|
||||
* meow_widget_hit/meow_listview_hit 判定命中控件/行)。已注册回调则分发;
|
||||
* 其抬手已被 touch_release 抑制(不再产生 TAP/SWIPE),此处无需吞后续 tap。 */
|
||||
if (in_startup) continue;
|
||||
/* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */
|
||||
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
|
||||
if (g_long_press_cb) g_long_press_cb(e.a, e.b);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t now = now_ms();
|
||||
if (g_events_set && g_events.on_tick) {
|
||||
if (now - last_tick_ms >= 16) {
|
||||
last_tick_ms = now;
|
||||
g_events.on_tick();
|
||||
}
|
||||
}
|
||||
|
||||
/* A-0 全局状态栏 1s 轻刷:电量/WiFi/音量 + 时钟。时钟每秒变 → 重画顶条并上屏,
|
||||
* 即使 APP 空闲(未主动 flush)也保持时钟走动。
|
||||
* 「同一个状态栏」闭环:仅系统进程(launcher)刷新顶条;App 进程不渲染状态栏,
|
||||
* 此处不再 flush 顶条(避免把 launcher 叠画的系统顶条冲成 App 背景)。数据仍刷新
|
||||
* (meow_ui_sys_refresh 无副作用),App 侧 inset/热区不受影响。 */
|
||||
static uint64_t last_sys_ms = 0;
|
||||
if (now - last_sys_ms >= 1000) {
|
||||
last_sys_ms = now;
|
||||
meow_ui_sys_refresh();
|
||||
if (meow_ui_sys_render_enabled() && meow_ui_sys_statusbar_active())
|
||||
meow_ui_flush_region(0, 0, meow_ui_width(), MUI_SBAR_H);
|
||||
}
|
||||
|
||||
if (meow_ime_active()) { /* 软键盘覆盖层随帧绘制(30fps 节流,省 CPU) */
|
||||
static uint64_t last_ime_ms = 0;
|
||||
uint64_t n = now_ms();
|
||||
if (n - last_ime_ms >= 33) {
|
||||
last_ime_ms = n;
|
||||
meow_ime_draw();
|
||||
meow_ui_flush();
|
||||
}
|
||||
if (!got) usleep(16000);
|
||||
} else if (!got) {
|
||||
usleep(16000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* M2.71-IME-fix:供 meow_ui_ime.c 的阻塞 IME 循环周期性喂 App on_tick。
|
||||
* 阻塞 IME 循环不经过 meow_ui_run,无法靠主循环喂 tick,故在此开独立入口;
|
||||
* 与 meow_ui_run 共用 last_tick_ms(二者互斥运行,无竞争)。调用方在 meow_ime_draw 之前调用,
|
||||
* 使 App 重绘先进 back_buf、IME 面板随后覆盖(IME 始终最上,不被 App 重绘盖住)。 */
|
||||
void meow_ui_feed_tick(void)
|
||||
{
|
||||
if (!(g_events_set && g_events.on_tick)) return;
|
||||
uint64_t now = now_ms();
|
||||
if (now - last_tick_ms < 16) return;
|
||||
last_tick_ms = now;
|
||||
g_events.on_tick();
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* meow_ui_internal.h — SDK 内部各 TU 共享声明(不随 meow_ui.h 发布给应用)。
|
||||
* 仅供 meow_ui_*.c 之间协作:渲染后端底层能力 + 输入事件队列 + 主题取用。
|
||||
*/
|
||||
#ifndef MEOW_UI_INTERNAL_H
|
||||
#define MEOW_UI_INTERNAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "meow_ui.h"
|
||||
|
||||
/* ---- 系统状态喂入(Stage-One 状态栏收敛;meow_ui_sys.c 提供)----
|
||||
* launcher(系统进程)不跑 SDK 事件循环(无 meow_ui_run),SDK 的 meow_ui_sys_refresh()
|
||||
* 不会在 launcher 里自动触发;且电量/音量/亮度等权威值本就在 launcher 的 g_sys 快照里
|
||||
* (launcher 是背光/音量写入方)。故此 API 让 launcher 把权威快照喂进 SDK 的系统状态,
|
||||
* 喂入即置状态栏 force,下一次 present() 重画顶条。SDK 自身 meow_ui_sys_refresh() 仍按
|
||||
* 同源兜底(供 App 流程 meow_ui_run 使用)。只填 SDK 快照所需最小字段,与 launcher 的
|
||||
* meow_sys_state(util.c)字段名交叉映射,二者是不同 TU 的异名类型,故用此
|
||||
* 专用喂入结构,避免 launcher 引入 meow_ui_sys.h 造成 typedef 重定义。 */
|
||||
typedef struct meow_ui_feed {
|
||||
int batt; /* 电量 %,-1 未知 */
|
||||
int charging; /* 1 充电/满 0 放电 */
|
||||
int wifi_on; /* 1 网卡 up(射频开) */
|
||||
int wifi_connected;/* 1 已关联 AP(operstate up);WiFi 图标分档用 */
|
||||
int wifi_level; /* 信号 dBm(<-1000=未知);≥-60 强 / -75 中 / 更弱弱 */
|
||||
int bt_seen; /* hci0 节点存在 */
|
||||
int bt_on; /* 实际上电 */
|
||||
int vol; /* 音量 0-100 */
|
||||
int brightness; /* 背光 1-100;<=0 视为未知/屏关不入顶条 */
|
||||
} meow_ui_feed;
|
||||
void meow_ui_sys_feed(const meow_ui_feed *f); /* 喂权威快照 + 置 force 重绘顶条 */
|
||||
|
||||
/* ---- 系统 UI 模式(「同一个状态栏」闭环,§3.1 / FRAMEWORK_DESIGN §10.2)----
|
||||
* 状态栏 = 系统级组件,仅系统进程(launcher)渲染。SDK 在 App 进程与 launcher 进程都链接,
|
||||
* 但顶条/提示线的**渲染**仅当本进程为系统 UI(launcher 置 system_mode=1)才进行;App 进程
|
||||
* 不渲染(meow_ui_sys_statusbar_active()/meow_ui_statusbar_height() 不受此开关影响 → App 仍
|
||||
* inset 28、热区可用,只是不画顶条、由 launcher 叠加)。launcher init 后调
|
||||
* meow_ui_sys_set_system_mode(1);App 保持默认 0。 */
|
||||
void meow_ui_sys_set_system_mode(int on); /* 1=本进程为系统 UI(渲染顶条/提示线) */
|
||||
int meow_ui_sys_render_enabled(void); /* 当前进程是否渲染系统顶条 */
|
||||
/* 把全局状态栏画进本进程 back_buf 顶部 MUI_SBAR_H 行(仅系统进程调用,见 render_enabled)。
|
||||
* launcher 的 App 前台叠加(launch_app 循环)直接调它 + 标脏顶条上屏。实现见 meow_ui_sys.c。 */
|
||||
void meow_ui_sys_statusbar_render(void);
|
||||
/* 收合「上移出屏」过渡渲染:把顶条内容整体上移 yoff 像素并裁顶(y<0 处自然剪裁,meow_font/图标
|
||||
* blit 已带边界钳制),仅画可见条高 [0, MUI_SBAR_H-yoff) 行(其上为 App 内容、由 launcher 保留)。
|
||||
* yoff=0 等价 meow_ui_sys_statusbar_render()。实现见 meow_ui_sys.c。 */
|
||||
void meow_ui_sys_statusbar_render_off(int yoff);
|
||||
|
||||
/* ---- 状态栏 MD3 图标簇(meow_ui_sys.c 渲染右侧;位图在 SDK 侧 sb_icons.h)----
|
||||
* meow_ui_gfx.c 的 meow_gfx_blit_icon(idx,...) 用这些索引查 sb_icons[]。
|
||||
* 索引值与 launcher sb_icons.h 的 SB_I_* 一一对应(位图同源,勿改序)。 */
|
||||
#define MEOW_SB_ICON_WIFI 0
|
||||
#define MEOW_SB_ICON_BT 1
|
||||
#define MEOW_SB_ICON_BATT 2
|
||||
#define MEOW_SB_ICON_VOL 3
|
||||
#define MEOW_SB_ICON_BRIGHT 4
|
||||
#define MEOW_SB_ICON_WIFI_STRONG 5
|
||||
#define MEOW_SB_ICON_WIFI_MEDIUM 6
|
||||
#define MEOW_SB_ICON_WIFI_WEAK 7
|
||||
#define MEOW_SB_ICON_WIFI_NO 8
|
||||
#define MEOW_SB_ICON_WIFI_OFF 9
|
||||
#define MEOW_SB_ICON_BT_NO 10
|
||||
#define MEOW_SB_ICON_BT_CONN 11
|
||||
#define MEOW_SB_ICON_BT_DIS 12
|
||||
#define MEOW_SB_ICON_BT_SEARCH 13
|
||||
#define MEOW_SB_ICON_BATT_CHG 14
|
||||
|
||||
/* 状态栏右下布局常量(与 launcher SB_* 对齐):图标右缘↔文字间隙 / 相邻项间隙 / 图标目标 ink 高 /
|
||||
* 右簇左缘相对时钟右缘安全间距 / 图标 ink 垂直中心(=状态栏文字中心,28px 栏约 14)。 */
|
||||
#define MEOW_SB_TGAP 6
|
||||
#define MEOW_SB_XGAP 10
|
||||
#define MEOW_SB_ICON_INK 16
|
||||
#define MEOW_SB_CLK_MARGIN 14
|
||||
#define MEOW_SB_ICON_CY 14
|
||||
#define MEOW_SB_ICON_SIZE 32 /* 图标槽宽(= sb_icons.h SB_ICON_SIZE,勿改序) */
|
||||
|
||||
/* 把第 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);
|
||||
|
||||
/* ---- 顶级主题风格族(§8.1.2,minimal):状态栏图标集/配色随 ui.style(md3/miuix)----
|
||||
* meow_ui_theme.c 实现:读 settings.conf 的 ui.style(md3→0 / miuix→1),缓存,随
|
||||
* meow_theme_reload() 重读。launcher settings 改 ui.style 后经 sb_sdk_refresh→theme_reload
|
||||
* →本值更新 → 状态栏图标集跟手切。公共版见 meow_ui.h(meow_ui_set_style/meow_ui_style)。 */
|
||||
int meow_ui_style(void);
|
||||
|
||||
/* ---- 主题(meow_ui_theme.c 提供)----
|
||||
* 重置主题缓存,使下一次 meow_theme_get() 重读 settings.conf(display.theme /
|
||||
* display.theme_mode / display.theme_dark_start|end)。launcher 在 statusbar 刷新前调用,
|
||||
* 让 SDK 状态栏(其主题是缓存的)跟手 launcher 侧主题变更,避免「launcher 界面已换肤、
|
||||
* 状态栏滞留旧主题」。App 无需调用(主题变更本就由 Settings 改 settings.conf,下一个
|
||||
* set_mode/get 自然读到——缓存仅挡「同会话内 settings 被外部改写」的即时反映)。 */
|
||||
void meow_theme_reload(void);
|
||||
|
||||
/* ---- 渲染后端(meow_ui_gfx.c)提供 ---- */
|
||||
/* 把 RGBA 像素(stb_image 输出,4 通道 straight alpha)blit 进 back_buf;
|
||||
* alpha 为全局不透明度 0-255(255 时仍用像素自身 alpha 做逐像素混合)。 */
|
||||
void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha);
|
||||
|
||||
/* 标记一个区域脏(供非标准绘制使用)。 */
|
||||
void meow_gfx_mark_dirty(int x, int y, int w, int h);
|
||||
|
||||
/* flush 挂起(IME/模态阻塞循环用):on=1 期间 meow_ui_flush/flush_region 只记脏不上屏,
|
||||
* 解除后由调用方一次 flush 原子上屏,避免覆盖层与 App on_tick 重绘同帧两刷频闪。 */
|
||||
void meow_gfx_flush_hold(int on);
|
||||
|
||||
/* M2.78:安装 SIGTERM/SIGINT 退出日志兜底处理器(meow_ui_log.c;meow_ui_init 调用)。 */
|
||||
void meow_log_install_exit_handler(void);
|
||||
|
||||
/* M2.87:安装 SIGUSR1 无害护栏处理器(meow_ui_log.c;meow_ui_init 调用)。
|
||||
* SDK 框架 App 的左缘返回由 SDK on_swipe 处理,watchdog 对框架 App 不再发 SIGUSR1;
|
||||
* 此护栏兜底任何漏进的 SIGUSR1(看门狗判定误差/竞态/测试路径):记日志+忽略,绝不杀 App。 */
|
||||
void meow_ui_install_sigusr1_guard(void);
|
||||
|
||||
/* 绘制期裁剪(MEOW_APP_SDK.md §3,2026-08-31):当前 clip 矩形内才写像素,默认全屏。
|
||||
* meow_widget_draw(w) 画任何控件前 push 该控件矩形、画完 pop——任何控件(含 W_CUSTOM)
|
||||
* 画不出自身边界,根治自定义画布越界脏区。可嵌套(入栈/出栈,push 与当前 clip 求交)。
|
||||
* 顶层 clip=全屏,故 IME/模态(snapshot+整帧路径)经 gfx 原语直绘全屏 scrim 时不受裁剪;
|
||||
* meow_gfx_flush_hold 语义不依赖 clip(flush 只在解除后原子上屏)。 */
|
||||
void meow_gfx_clip_push(int x, int y, int w, int h);
|
||||
void meow_gfx_clip_pop(void);
|
||||
|
||||
/* 标记整帧脏(launcher 手动脏模型桥接:自绘 raw 像素后标全屏,present 走 SDK)。 */
|
||||
void meow_gfx_mark_full(void);
|
||||
|
||||
/* 清空脏区(含整帧标记)。用于 App 前台叠加路径:meow_ui_statusbar_set_collapsed 会标整帧脏,
|
||||
* 但 launcher 在 App 上只应 blit 顶条(可见条高),绝不能整帧 blit(会把自身 back_buf 的 stale
|
||||
* 菜单内容盖到 App 内容上)——叠加前用本函数撤销整帧脏,再按需标局部脏(agent-146)。 */
|
||||
void meow_gfx_dirty_reset(void);
|
||||
|
||||
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
|
||||
unsigned char *meow_ui_back_buf(void);
|
||||
/* back_buf 的每行字节数(pitch)与位深(16/32)。M2.71-bug3:模态/IME 快照恢复用。 */
|
||||
int meow_ui_back_buf_pitch(void);
|
||||
int meow_ui_back_buf_bpp(void);
|
||||
|
||||
/* 实心圆角矩形(控件按钮底色用;公共 §3 只有空心 draw_rect_round)。[D3] */
|
||||
void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color);
|
||||
/* 圆角矩形半透明填充(M2.71-bug4):禁用变暗等状态层用,圆角外不叠色(防灰方块)。 */
|
||||
void meow_gfx_fill_round_alpha(int x, int y, int w, int h, int r, uint32_t color, int alpha);
|
||||
|
||||
/* §5.8.6 batch2:软渲圆形/线段/圆弧环原语(无 GPU、无 -lm,内部整数/查表实现)。
|
||||
* 供 Radio(外圈/内点)、Badge(红点)、ProgressBar(环形轨道/进度弧)、Checkbox(勾线段)使用。 */
|
||||
void meow_gfx_fill_circle(int cx, int cy, int r, uint32_t color); /* 实心圆(bbox 距离测试) */
|
||||
void meow_gfx_line(int x0, int y0, int x1, int y1, int th, uint32_t color); /* 粗线(Bresenham + 圆帽) */
|
||||
void meow_gfx_ring_arc(int cx, int cy, int r, int th, int start_deg,
|
||||
int sweep_deg, uint32_t color); /* 圆弧环(查表采样成粗线) */
|
||||
|
||||
/* 是否运行于真实 fb 之上(0 = 头less/离屏,qemu/宿主)。 */
|
||||
int meow_gfx_is_device(void);
|
||||
|
||||
/* §10.5 叠加让位:present 因 /tmp/meow_notif_active 存在而被自抑的累计次数
|
||||
* (仅 App 进程会自抑;宿主单测断言用,设备上无调用方)。 */
|
||||
unsigned long long meow_gfx_overlay_suppressed_count(void);
|
||||
|
||||
/* 中文/日文字库(mfont)是否已加载(状态栏「蓝牙」CJK 与回退 "BT" 二选一)。 */
|
||||
int meow_gfx_mfont_ready(void);
|
||||
|
||||
/* ---- 输入层(meow_ui_input.c)---- */
|
||||
/* 语义事件:由输入线程解析后压入队列,run/IME 循环弹出。 */
|
||||
#define MEOW_EV_KEY 1 /* a=keycode, b=1 按下 / 0 抬起 */
|
||||
#define MEOW_EV_TAP 2 /* a=x, b=y(触摸抬起落点) */
|
||||
#define MEOW_EV_SWIPE 3 /* a=MEOW_SWIPE_*, b=距离(px) */
|
||||
#define MEOW_EV_SWIPE_PROGRESS 4 /* a=按下X(t_dx), b=当前X(t_cx);dx=b-a(>0=左缘向内划=返回手势) */
|
||||
#define MEOW_EV_TOUCH_DOWN 6 /* a=x, b=y:按下后首个有效坐标(拖动起点,M2.71-bug2/6) */
|
||||
#define MEOW_EV_TOUCH_MOVE 7 /* a=x, b=y:拖动过程坐标变化(坐标变才入队;跟手用) */
|
||||
#define MEOW_EV_LONG_PRESS 8 /* a=x, b=y:按下 ≥ 阈值未抬手(长按触发;其抬手不再产生 TAP/SWIPE) */
|
||||
|
||||
typedef struct meow_input_ev {
|
||||
uint8_t type;
|
||||
int a, b;
|
||||
} meow_input_ev;
|
||||
|
||||
/* 启动输入线程(幂等)。无设备时仅空转。 */
|
||||
void meow_input_start(void);
|
||||
/* 从队列弹出一个事件;无事件返回 0。 */
|
||||
int meow_input_pop(meow_input_ev *ev);
|
||||
/* 注入一条语义事件(宿主验证手势分发/进度条用;设备上无调用方)。 */
|
||||
void meow_input_inject(int type, int a, int b);
|
||||
/* 清空队列(IME 打开前丢弃残留触摸,避免误敲)。 */
|
||||
void meow_input_drain(void);
|
||||
|
||||
/* ---- M2.71-bug5:App 活动 → 通知 launcher 重置息屏计时 ----
|
||||
* launcher 前台 app 会话的 app_last_input 只在物理键边沿重置(它看不到应用内触摸)。SDK 在
|
||||
* 收到任意交互事件(tap/swipe/拖动/IME/模态)时节流 touch 出 /tmp/meow_app_activity 标记;
|
||||
* launcher app 循环探测到即 unlink + 重置计时(若已自动息屏则照 key-wake 路径唤醒)。 */
|
||||
void meow_ui_activity_ping(void);
|
||||
|
||||
/* ---- 实时触摸态(供控件绘制按压/按住视觉;输入线程写、app 主线读)----
|
||||
* 与语义事件队列无关:这些直接读输入线程的触摸状态机当前值,让 meow_widget_draw
|
||||
* 在「手指按下期间」能如实反映按钮的按下/按住视觉,而无需 app 自行追踪按下。 */
|
||||
int meow_input_touch_down(void); /* 当前是否有触摸按下 */
|
||||
int meow_input_touch_x(void); /* 当前 X(无按下=-1) */
|
||||
int meow_input_touch_y(void); /* 当前 Y(无按下=-1) */
|
||||
unsigned int meow_input_touch_hold_ms(void); /* 按下已持续毫秒(抬起后 0) */
|
||||
|
||||
/* 宿主单测/内部注入:模拟一次按压。down=1 时 x,y 为压点、hold_ms 可指定「已按住时长」
|
||||
* (把按下时刻回拨),便于测按住态;down=0 复位。设备运行时无调用方。 */
|
||||
void meow_input_touch_set(int down, int x, int y, unsigned hold_ms);
|
||||
|
||||
/* ---- 通用长按(§6.3,2026-08-31)----
|
||||
* 主循环/宿主调用:检测「按下 ≥ 阈值且位移 ≤ 容忍」的跨越;触发则入队 MEOW_EV_LONG_PRESS
|
||||
* 并返回 1(幂等:一次按压只触发一次)。供 meow_ui_run 每帧调用与宿主单测模拟时序。 */
|
||||
int meow_input_long_press_poll(void);
|
||||
/* 宿主单测/内部注入:模拟一次抬手(真实 touch_release;含长按后抑制 TAP/SWIPE)。设备运行时无调用方。 */
|
||||
void meow_input_touch_release(void);
|
||||
/* 宿主单测/内部注入:模拟按下后移动到 (x,y)(保原点、更新当前坐标;位移超长按容忍即取消)。设备运行时无调用方。 */
|
||||
void meow_input_touch_move(int x, int y);
|
||||
|
||||
/* ---- 控件层实时刷新(meow_ui_widget.c)----
|
||||
* 每帧由 meow_ui_run 调用(主线程):实时触摸按下/释放/按住达阈值时,重绘命中的按钮区
|
||||
* 并局部上屏,让按压/按住/释放视觉即时反映,无需 app 在 on_tick/on_tap 重绘(agent-119)。
|
||||
* 仅作用于 SDK 控件按钮(meow_widget_draw 的 W_BTN);app 纯自绘不适用。 */
|
||||
void meow_widget_touch_refresh(void);
|
||||
|
||||
/* ---- App 跨 Activity 全局控件(meow_ui_global.c,§6.1 ②骨架)----
|
||||
* 渲染帧(meow_ui_gfx present())在 active activity 内容之上、全局状态栏之下调用
|
||||
* meow_ui_global_render;触摸 TAP 分发先给 meow_ui_global_hit(命中返回 1=消费,
|
||||
* 否则落到 active activity 页面控件)。跨 Activity 常驻,由 SDK 调度。 */
|
||||
void meow_ui_global_render(void);
|
||||
int meow_ui_global_hit(int x, int y);
|
||||
|
||||
/* ---- 绘制物注册表(meow_ui_draw.c,§6.2 绘制物注册表/z序/区域重画)----
|
||||
* 内部协作接口:widget.c 在 create 末尾自动注册、meow_widget_free 自动注销;view.c 在
|
||||
* meow_view_add_widget/meow_view_free 挂/摘 widget 与 view 的 z 深度源;live_btn 收敛查询
|
||||
* 是否已注册(已注册→走 invalidate 区域重画,否则回退单控件 flush)。 */
|
||||
struct meow_view; /* 仅前向声明;完整类型在 meow_ui_view.h */
|
||||
|
||||
void meow_draw_widget_register(struct meow_widget *w); /* create 末尾:自动注册(幂等) */
|
||||
void meow_draw_widget_unregister(struct meow_widget *w); /* meow_widget_free:自动注销 */
|
||||
int meow_draw_widget_registered(const struct meow_widget *w);/* 是否在注册表(live_btn 收敛用) */
|
||||
void meow_draw_widget_set_view(struct meow_widget *w, struct meow_view *v); /* 挂 view:重算 auto z */
|
||||
void meow_draw_view_register(struct meow_view *v); /* view create:注册绘制物(on_draw 用) */
|
||||
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=清除回退自动) */
|
||||
|
||||
/* 控件布局深度反推(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 */
|
||||
int meow_widget_nchild(const struct meow_widget *w); /* 布局子项数 */
|
||||
const struct meow_widget *meow_widget_child_at(const struct meow_widget *w, int idx);
|
||||
|
||||
/* 控件几何/归属访问器(meow_ui_draw.c 用:绘制物 rect 实时读 + owner 过滤)。 */
|
||||
int meow_widget_owner(const struct meow_widget *w); /* 创建时绑定的 Activity */
|
||||
void meow_widget_rect(const struct meow_widget *w, int out[4]); /* x,y,w,h */
|
||||
|
||||
/* ---- IME(meow_ui_ime.c)---- */
|
||||
/* 当前是否显示软键盘。 */
|
||||
int meow_ime_active(void);
|
||||
/* 软键盘拦截一次触摸。返回 1=已消费(供 IME 转键/提交),0=未命中(透传给 app)。 */
|
||||
int meow_ime_handle_tap(int x, int y);
|
||||
/* 软键盘绘制覆盖层(每次 flush 前调用;覆盖底部约 40%)。 */
|
||||
void meow_ime_draw(void);
|
||||
/* 关闭 IME 并交付最终结果(result: 1=确认 0=取消)。 */
|
||||
void meow_ime_close(int result);
|
||||
|
||||
/* 拼音 IME 事件钩子(meow_ui_input.c 主循环 / meow_ui_ime.c 阻塞循环调用,IME 打开时):
|
||||
* 统一分发 TAP/TOUCH_DOWN/SWIPE/LONG_PRESS 给拼音模式(组合/候选栏翻页/长按清空)。
|
||||
* 非拼音(ASCII/NUM)模式:TAP 走 meow_ime_handle_tap,其余事件返回 0(沿用既有英文行为,零回归)。
|
||||
* 返回 1=已消费(调用方 continue),0=透传。 */
|
||||
int meow_ime_handle_event(const meow_input_ev *e);
|
||||
/* 当前 IME 是否拼音模式(供 meow_ui_input.c 决定是否喂长按轮询)。 */
|
||||
int meow_ime_mode_pinyin(void);
|
||||
|
||||
#endif /* MEOW_UI_INTERNAL_H */
|
||||
@@ -0,0 +1,334 @@
|
||||
/*
|
||||
* meow_ui_log.c — MeowOS 应用统一日志 API(M2.52 版)。
|
||||
*
|
||||
* 背景:此前 app 各自造轮子(如 hello.c hellolog() 自读 MEOW_LOG_DIR + snprintf +
|
||||
* fopen/fprintf),重复、易错、无分离/级别。本文件实现 SDK 统一 meow_log():
|
||||
*
|
||||
* - app 一行调用 meow_log(MEOW_LOG_INFO, "start")(见 meow_ui.h);
|
||||
* - SDK 启动时读 env:MEOW_LOG_DIR(日志目录,launcher 已注入到 logs/<app_id>,含 app 分层)、
|
||||
* MEOW_LOG_APP(app_id,无则 "app")、MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写
|
||||
* `<log_dir>/<启动时间>_<pid>[._<n>].log`(目录自动 mkdir -p),
|
||||
* 按 app 分目录 + 每次启动新文件(不同启动=不同文件,同一次启动内 O_APPEND 追加),
|
||||
* 级别过滤(lvl < level 丢弃);
|
||||
* - 格式:`<monotonic_ms> [<app_id>] <LEVEL> message\n`(meow_ui_time_ms() 取毫秒);
|
||||
* - 无 log_dir(无卡)/目录不可建/写失败 → 降级 stderr,不阻塞 app 不崩。
|
||||
*
|
||||
* 上限(用户定,防撑爆 TF 卡):
|
||||
* - 单文件 ≥ MEOW_LOG_FILE_MAX(2MB)→ 关当前 fd,开新文件并追加序号 `_<n>`;
|
||||
* - 每 App 日志总量 ≥ MEOW_LOG_TOTAL_MAX(50MB)→ 按期删最旧文件(按 mtime),
|
||||
* 节流(每 MEOW_LOG_CLEANUP_EVERY 次写入检查一次,另在首次打开/每次轮转时检查),
|
||||
* 避免高频日志时逐行 readdir/stat。
|
||||
*
|
||||
* 约定:与 meow_ui_gfx.c 同风格(M2.x 中文注释)。线程安全(pthread_mutex,launcher
|
||||
* 当前台/后台进程都会在已链 -lpthread 的应用里调用)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
|
||||
/* 单行消息上限(超长截断) */
|
||||
#define MEOW_LOG_LINE_MAX 1024
|
||||
|
||||
/* 日志上限:单文件 2MB 开新文件;每 App 总量 50MB 删最旧 */
|
||||
#define MEOW_LOG_FILE_MAX (2 * 1024 * 1024)
|
||||
#define MEOW_LOG_TOTAL_MAX (50 * 1024 * 1024)
|
||||
#define MEOW_LOG_CLEANUP_MAX 128 /* 一次清理最多扫描文件数 */
|
||||
#define MEOW_LOG_CLEANUP_EVERY 2048 /* 每 N 次写入做一次总量清理(节流) */
|
||||
|
||||
/* 级别字符串表(与 meow_log_level 序号对齐) */
|
||||
static const char *g_lvl_str[] = { "DEBUG", "INFO", "WARN", "ERROR" };
|
||||
|
||||
/* 内部状态(各字段约定见头注释) */
|
||||
static pthread_mutex_t g_log_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_once_t g_log_once = PTHREAD_ONCE_INIT;
|
||||
|
||||
static char g_app_id[64] = "app"; /* env MEOW_LOG_APP,无则 "app" */
|
||||
static char g_log_dir[256] = ""; /* env MEOW_LOG_DIR,无/空则降级 stdio */
|
||||
static meow_log_level g_level = MEOW_LOG_INFO; /* 默认 INFO */
|
||||
|
||||
/* M2.52:日志文件为 `<log_dir>/<ts>_<pid>[._<n>].log`;M2.x 简化后 MEOW_LOG_DIR 已含 app 分层
|
||||
* (launcher 注入 logs/<app_id>)。g_log_base 存不含序号后缀的路径前缀 `<dir>/<ts>_<pid>`;
|
||||
* g_log_seq 为轮转序号(0=首个文件无后缀;>0 追加 `_<n>`)。两者均在 log_init()
|
||||
* (pthread_once 那次)里生成一次。 */
|
||||
static char g_log_base[384] = "";
|
||||
static char g_log_file[512] = "";
|
||||
static int g_log_seq = 0;
|
||||
|
||||
static int g_log_fd = -1; /* 当前日志 fd;-1=未开 */
|
||||
static int g_log_fallback = 0; /* 1=无卡/写失败,降级 stderr */
|
||||
static int g_log_open_tried = 0; /* 是否已尝试开首个文件(只试一次,失败则降级) */
|
||||
static unsigned long g_write_count = 0; /* 累计写入次数(周期总量清理节流) */
|
||||
|
||||
static void log_open(void); /* 前向声明(log_sig_exit 在其定义之前使用) */
|
||||
|
||||
/* 由 g_log_base + g_log_seq 重建当前文件路径(seq=0 无后缀,>0 追加 _<n>)。 */
|
||||
static void log_rebuild_file(void)
|
||||
{
|
||||
if (g_log_seq <= 0)
|
||||
snprintf(g_log_file, sizeof g_log_file, "%s.log", g_log_base);
|
||||
else
|
||||
snprintf(g_log_file, sizeof g_log_file, "%s_%d.log", g_log_base, g_log_seq);
|
||||
}
|
||||
|
||||
/* 一次性初始化:读 env 决定 app_id/log_dir/level;构建 g_log_base 与首个 g_log_file。
|
||||
* (pthread_once 保证仅一次。) */
|
||||
static void log_init(void)
|
||||
{
|
||||
const char *s;
|
||||
|
||||
/* app_id:env MEOW_LOG_APP,无则 "app" */
|
||||
s = getenv("MEOW_LOG_APP");
|
||||
if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s);
|
||||
|
||||
/* log_dir:env MEOW_LOG_DIR,无/空则 ""(降级 stdio) */
|
||||
s = getenv("MEOW_LOG_DIR");
|
||||
if (s && s[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", s);
|
||||
|
||||
/* M2.52:构建完整前缀 `<log_dir>/<ts>_<pid>`(M2.x 简化:MEOW_LOG_DIR 已含 app 分层)。
|
||||
* ts 用 localtime 的 %Y%m%d_%H%M%S(启动时刻);尾拼 _<pid> 防同秒重复启动撞名,
|
||||
* 从而每次启动=新文件,同一次启动内 O_APPEND 追加。若 log_dir 为空(无卡)
|
||||
* 则跳过(log_open 降级 stderr)。 */
|
||||
if (g_log_dir[0]) {
|
||||
char ts[32];
|
||||
time_t now = time(NULL);
|
||||
struct tm tmnow;
|
||||
localtime_r(&now, &tmnow);
|
||||
strftime(ts, sizeof ts, "%Y%m%d_%H%M%S", &tmnow);
|
||||
snprintf(g_log_base, sizeof g_log_base, "%s/%s_%d",
|
||||
g_log_dir, ts, (int)getpid());
|
||||
log_rebuild_file();
|
||||
}
|
||||
|
||||
/* level:env MEOW_LOG_LEVEL 可选覆盖(系统下发;默认 INFO) */
|
||||
s = getenv("MEOW_LOG_LEVEL");
|
||||
if (s && s[0]) {
|
||||
if (strcmp(s, "DEBUG") == 0) g_level = MEOW_LOG_DEBUG;
|
||||
else if (strcmp(s, "WARN") == 0) g_level = MEOW_LOG_WARN;
|
||||
else if (strcmp(s, "ERROR") == 0) g_level = MEOW_LOG_ERROR;
|
||||
else g_level = MEOW_LOG_INFO;
|
||||
}
|
||||
}
|
||||
|
||||
/* 退出日志兜底(M2.78):App 被 watchdog 返回键 SIGTERM 时走默认终止,跳过正常
|
||||
* 路径的 meow_log("exit"),日志只剩 start。SDK 在 meow_ui_init 安装本处理器:
|
||||
* SIGTERM/SIGINT → 直接 write 一条 exit:<sig>(绕过 stdio 缓冲)→ exit(0)(干净码)。
|
||||
* App 自装处理器(如 hello 画 BYE)会覆盖本处理器,其 exit(0) 路径不受影响。 */
|
||||
static void log_sig_exit(int sig)
|
||||
{
|
||||
if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); }
|
||||
if (g_log_fd >= 0) {
|
||||
char line[128];
|
||||
int n = snprintf(line, sizeof line, "%llu [%s] INFO exit:%s\n",
|
||||
(unsigned long long)meow_ui_time_ms(), g_app_id,
|
||||
sig == SIGTERM ? "sigterm" : "sigint");
|
||||
if (n > 0) { ssize_t w = write(g_log_fd, line, (size_t)n); (void)w; }
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
void meow_log_install_exit_handler(void)
|
||||
{
|
||||
signal(SIGTERM, log_sig_exit);
|
||||
signal(SIGINT, log_sig_exit);
|
||||
}
|
||||
|
||||
/* M2.87:SIGUSR1 无害护栏(防线冗余)。watchdog 对「左缘起点+右滑」发 SIGUSR1 当返回事件;
|
||||
* SDK 框架 App(framework=1)自己经 on_swipe 处理左缘返回(meow_ui_input.c),不应被 SIGUSR1
|
||||
* 终止。但看门狗对 framework 判定有误/竞态/测试注入等**任何**路径漏进 SIGUSR1,默认动作都会
|
||||
* 杀死 App(ncm signal=16 崩溃根因之一)。本节安装一个不做任何致命动作的处理器:记日志+忽略
|
||||
* ——框架 App 的返回/退出语义由 SDK on_swipe 统一决策,SIGUSR1 仅作「绝不杀 App」的保活护栏,
|
||||
* 不重复触发返回(避免与 on_swipe 双重返回)。App 若自装 SIGUSR1 处理器(hello/ncm 均未装,
|
||||
* 裸沉浸 App 自绘处理)会覆盖本处理器;`--sim-swipe` 测试路径也随之被吞。 */
|
||||
static void sigusr1_guard(int sig)
|
||||
{
|
||||
(void)sig;
|
||||
if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); }
|
||||
char line[128];
|
||||
int n = snprintf(line, sizeof line, "%llu [%s] WARN sigusr1: ignored (guard)\n",
|
||||
(unsigned long long)meow_ui_time_ms(), g_app_id);
|
||||
if (n > 0) {
|
||||
if (g_log_fd >= 0) { ssize_t w = write(g_log_fd, line, (size_t)n); (void)w; }
|
||||
else { ssize_t w = write(2, line, (size_t)n); (void)w; } /* 无日志目录:写 stderr,护栏可观测 */
|
||||
}
|
||||
/* 不 exit、不返回处理:返回交给 on_swipe/SDK。这里仅记录并吞掉。 */
|
||||
}
|
||||
void meow_ui_install_sigusr1_guard(void)
|
||||
{
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_handler = sigusr1_guard;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0; /* 不设 SA_RESTART:被中断的 poll/read 返回按 EINTR 处理后继续 */
|
||||
sigaction(SIGUSR1, &sa, NULL);
|
||||
}
|
||||
|
||||
/* mkdir -p:逐分量 mkdir(已存在 EEXIST 视为成功)。返回 0=成功,-1=失败。 */
|
||||
static int log_mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[256];
|
||||
size_t len, i;
|
||||
if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1;
|
||||
len = strlen(path);
|
||||
memcpy(tmp, path, len + 1);
|
||||
if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; /* 去尾斜杠 */
|
||||
for (i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* M2.52:对 `<log_dir>`(M2.x 简化后已含 app 分层)增量 mkdir -p,并打开 g_log_file
|
||||
* (O_APPEND|O_CREAT;同一次启动内追加,不同启动=不同文件)。失败置 g_log_fallback。
|
||||
* 首次由 meow_log 经 g_log_open_tried 守卫调用;轮转后由 meow_log 再次调用(目录已存在,
|
||||
* mkdir_p 幂等)。仅在持有 g_log_mu 时调用(g_log_base 已构建)。 */
|
||||
static void log_open(void)
|
||||
{
|
||||
char logs[512];
|
||||
if (!g_log_dir[0] || !g_log_file[0]) { g_log_fallback = 1; return; }
|
||||
if (snprintf(logs, sizeof logs, "%s", g_log_dir) >= (int)sizeof logs) {
|
||||
g_log_fallback = 1; return;
|
||||
}
|
||||
if (log_mkdir_p(logs) != 0) { g_log_fallback = 1; return; }
|
||||
g_log_fd = open(g_log_file, O_WRONLY | O_APPEND | O_CREAT, 0644);
|
||||
if (g_log_fd < 0) g_log_fallback = 1;
|
||||
}
|
||||
|
||||
/* 清理用文件记录(按 mtime 升序删最旧) */
|
||||
typedef struct { char name[384]; time_t mtime; long size; } log_spill_t;
|
||||
|
||||
static int log_spill_cmp(const void *a, const void *b)
|
||||
{
|
||||
const log_spill_t *fa = a, *fb = b;
|
||||
if (fa->mtime < fb->mtime) return -1;
|
||||
if (fa->mtime > fb->mtime) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 每 App 总量清理:扫描 `<log_dir>/`(M2.x 简化后已含 app 分层)下所有 .log(跳过当前正写的
|
||||
* g_log_file),累计大小,超过 MEOW_LOG_TOTAL_MAX 则按 mtime 从最旧删到 ≤ 阈值。
|
||||
* 只规整本 App 目录(进程隔离),节流调用(见 meow_log)。仅在持有 g_log_mu 时调用。 */
|
||||
static void log_cleanup_old(void)
|
||||
{
|
||||
DIR *d;
|
||||
struct dirent *e;
|
||||
char logs[512], path[384];
|
||||
log_spill_t files[MEOW_LOG_CLEANUP_MAX];
|
||||
int n = 0, i;
|
||||
long total = 0;
|
||||
|
||||
if (!g_log_dir[0] || !g_log_file[0]) return;
|
||||
if (snprintf(logs, sizeof logs, "%s", g_log_dir)
|
||||
>= (int)sizeof logs) return;
|
||||
d = opendir(logs);
|
||||
if (!d) return;
|
||||
|
||||
while ((e = readdir(d)) != NULL) {
|
||||
struct stat st;
|
||||
size_t nl;
|
||||
if (e->d_name[0] == '.') continue;
|
||||
nl = strlen(e->d_name);
|
||||
if (nl < 5 || strcmp(e->d_name + nl - 4, ".log") != 0) continue; /* 只看 .log */
|
||||
if (snprintf(path, sizeof path, "%s/%s", logs, e->d_name) >= (int)sizeof path) continue;
|
||||
if (strcmp(path, g_log_file) == 0) continue; /* 跳过当前正写文件,避免删 unlink 后仍写 */
|
||||
if (stat(path, &st) != 0) continue;
|
||||
if (!S_ISREG(st.st_mode)) continue;
|
||||
if (n >= MEOW_LOG_CLEANUP_MAX) break;
|
||||
snprintf(files[n].name, sizeof files[n].name, "%s", path);
|
||||
files[n].mtime = st.st_mtime;
|
||||
files[n].size = (long)st.st_size;
|
||||
total += (long)st.st_size;
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (total <= MEOW_LOG_TOTAL_MAX) return;
|
||||
qsort(files, (size_t)n, sizeof files[0], log_spill_cmp);
|
||||
for (i = 0; i < n && total > MEOW_LOG_TOTAL_MAX; i++) {
|
||||
if (unlink(files[i].name) == 0) total -= files[i].size;
|
||||
}
|
||||
}
|
||||
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...)
|
||||
{
|
||||
char body[MEOW_LOG_LINE_MAX];
|
||||
char line[MEOW_LOG_LINE_MAX + 96];
|
||||
const char *lvl_s;
|
||||
va_list ap;
|
||||
int n;
|
||||
|
||||
pthread_once(&g_log_once, log_init);
|
||||
|
||||
pthread_mutex_lock(&g_log_mu);
|
||||
if (lvl < g_level) { pthread_mutex_unlock(&g_log_mu); return; } /* 级别过滤 */
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(body, sizeof body, fmt, ap);
|
||||
va_end(ap);
|
||||
body[sizeof body - 1] = '\0';
|
||||
|
||||
if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); }
|
||||
if (g_log_fd >= 0 && g_write_count == 0) log_cleanup_old(); /* 首次打开即清一次 */
|
||||
|
||||
/* 级别名钳位(防 app 传越界枚举值触发 OOB 读) */
|
||||
lvl_s = (lvl >= 0 && lvl <= MEOW_LOG_ERROR) ? g_lvl_str[lvl]
|
||||
: g_lvl_str[MEOW_LOG_ERROR];
|
||||
|
||||
if (g_log_fd >= 0) {
|
||||
struct stat fst;
|
||||
/* 单文件超限 → 轮转开新文件(_<n> 递增);fstat 每次写前检查(日志低频,开销可忽略) */
|
||||
if (fstat(g_log_fd, &fst) == 0 && fst.st_size >= (off_t)MEOW_LOG_FILE_MAX) {
|
||||
close(g_log_fd);
|
||||
g_log_fd = -1;
|
||||
g_log_seq++;
|
||||
log_rebuild_file();
|
||||
log_open();
|
||||
if (g_log_fd >= 0) log_cleanup_old(); /* 轮转后顺带清一次 */
|
||||
}
|
||||
if (g_log_fd >= 0) {
|
||||
n = snprintf(line, sizeof line, "%llu [%s] %s %s\n",
|
||||
(unsigned long long)meow_ui_time_ms(),
|
||||
g_app_id, lvl_s, body);
|
||||
if (n > 0) { ssize_t wr = write(g_log_fd, line, (size_t)n); (void)wr; }
|
||||
}
|
||||
/* 周期总量清理(节流,避免高频日志逐行 readdir/stat) */
|
||||
if (++g_write_count % MEOW_LOG_CLEANUP_EVERY == 0) log_cleanup_old();
|
||||
} else {
|
||||
/* 降级 stderr(无卡/写失败):不阻塞 app,不崩 */
|
||||
fprintf(stderr, "[%s] %s %s\n", g_app_id, lvl_s, body);
|
||||
}
|
||||
pthread_mutex_unlock(&g_log_mu);
|
||||
}
|
||||
|
||||
void meow_log_set_level(meow_log_level lvl)
|
||||
{
|
||||
pthread_once(&g_log_once, log_init);
|
||||
pthread_mutex_lock(&g_log_mu);
|
||||
g_level = lvl;
|
||||
pthread_mutex_unlock(&g_log_mu);
|
||||
}
|
||||
|
||||
meow_log_level meow_log_get_level(void)
|
||||
{
|
||||
meow_log_level l;
|
||||
pthread_once(&g_log_once, log_init);
|
||||
pthread_mutex_lock(&g_log_mu);
|
||||
l = g_level;
|
||||
pthread_mutex_unlock(&g_log_mu);
|
||||
return l;
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
/* meow_ui_media.c — App 侧媒体会话封装(SDK 公共件;见 meow_ui_media.h)。
|
||||
*
|
||||
* 传输:AF_UNIX SOCK_STREAM 到 MEOW_IPC_SOCK(缺省 /usr/data/meow_ipc.sock)。后台线程持有
|
||||
* 持久连接:HELLO(app_id, MF_PLAYBACK) 注册为媒体源 + poll 读 launcher 下发 MSG_PLAYCTL(经
|
||||
* 该连接 fd 回发,见 launcher meow_media.c send_playctl)。meow_ui_media_meta() 在调用线程
|
||||
* 直接经同一连接发 MSG_META(全双工:写 META、读 PLAYCTL 互不干扰)。
|
||||
*
|
||||
* 连接生命周期(best-effort):懒连(首次 meta / 注册回调时 connect+HELLO+ACK);
|
||||
* 断线(读失败/写失败)→ close+置 -1,下次 meta 重连;launcher 不在 → connect 失败 → 静默。
|
||||
* 线程安全:g_fd 读写经互斥;后台线程只在 poll/读失败时改 fd,meta 线程只在 connect 时改 fd。
|
||||
*/
|
||||
#include "meow_ui_media.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <poll.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_app[MEOW_APP_ID_MAX] = "";
|
||||
static int g_app_set = 0;
|
||||
static int g_fd = -1;
|
||||
static pthread_t g_thr;
|
||||
static int g_thr_on = 0;
|
||||
static volatile int g_quit = 0;
|
||||
static meow_media_playctl_fn g_cb = NULL;
|
||||
|
||||
/* 后台线程主体(定义在文件后段;on_playctl 懒起时引用)。 */
|
||||
static void *media_thread_main(void *arg);
|
||||
|
||||
static const char *sock_path(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_IPC_SOCK");
|
||||
return (e && e[0]) ? e : MEOW_IPC_PATH;
|
||||
}
|
||||
|
||||
static int connect_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;
|
||||
}
|
||||
|
||||
/* 写满 n 字节(EINTR 重试);失败返回 0。 */
|
||||
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;
|
||||
}
|
||||
|
||||
/* 组装并写出 16B(LE)帧头 + 负载。 */
|
||||
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;
|
||||
uint16_t ver = MEOW_IPC_VERSION;
|
||||
memcpy(hdr + 0, &magic, 4);
|
||||
memcpy(hdr + 4, &ver, 2);
|
||||
memcpy(hdr + 6, &type, 2);
|
||||
memcpy(hdr + 8, &req_id, 4);
|
||||
memcpy(hdr + 12, &len, 4);
|
||||
if (!write_all(fd, hdr, sizeof hdr)) return -1;
|
||||
if (len && payload && !write_all(fd, payload, len)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 读一帧头+负载;返回负载长(≥0),-1=断/坏帧。 */
|
||||
static int read_frame(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
if (!read_all(fd, hdr, sizeof hdr)) return -1;
|
||||
uint32_t magic, len; uint16_t ver;
|
||||
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2);
|
||||
memcpy(&len, hdr + 12, 4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||||
if (len > rcap) len = rcap;
|
||||
if (len > 0 && !read_all(fd, rbuf, len)) return -1;
|
||||
return (int)len;
|
||||
}
|
||||
|
||||
static void media_app_id(void)
|
||||
{
|
||||
if (g_app_set) return;
|
||||
const char *s = getenv("MEOW_LOG_APP");
|
||||
snprintf(g_app, sizeof g_app, "%s", (s && s[0]) ? s : "app");
|
||||
g_app_set = 1;
|
||||
}
|
||||
|
||||
/* (调用方已持 mu)连接 + HELLO(MF_PLAYBACK) + 读 ACK。成功存 g_fd 返回 fd;失败 -1。 */
|
||||
static int conn_locked(void)
|
||||
{
|
||||
if (g_fd >= 0) return g_fd;
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) return -1;
|
||||
media_app_id();
|
||||
m_hello_t h;
|
||||
memset(&h, 0, sizeof h);
|
||||
snprintf(h.app_id, sizeof h.app_id, "%s", g_app);
|
||||
h.version = 1;
|
||||
h.flags = MF_PLAYBACK;
|
||||
if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) != 0) { close(fd); return -1; }
|
||||
uint8_t rbuf[256];
|
||||
uint16_t rt;
|
||||
if (read_frame(fd, &rt, rbuf, sizeof rbuf) < 0 ||
|
||||
(rt != MSG_ACK && rt != MSG_ERR)) { close(fd); return -1; }
|
||||
g_fd = fd;
|
||||
return fd;
|
||||
}
|
||||
|
||||
/* (调用方已持 mu)标记断线并关闭。 */
|
||||
static void mark_dead_locked(void)
|
||||
{
|
||||
if (g_fd >= 0) { close(g_fd); g_fd = -1; }
|
||||
}
|
||||
|
||||
void meow_ui_media_init(const char *app_id)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (app_id && app_id[0]) {
|
||||
snprintf(g_app, sizeof g_app, "%s", app_id);
|
||||
g_app_set = 1;
|
||||
} else if (!g_app_set) {
|
||||
const char *s = getenv("MEOW_LOG_APP");
|
||||
snprintf(g_app, sizeof g_app, "%s", (s && s[0]) ? s : "app");
|
||||
g_app_set = 1;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
void meow_ui_media_on_playctl(meow_media_playctl_fn cb)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_cb = cb;
|
||||
int need_start = !g_thr_on;
|
||||
if (need_start) { g_quit = 0; g_thr_on = 1; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (need_start) pthread_create(&g_thr, NULL, media_thread_main, NULL);
|
||||
/* detach:App 退出经 meow_ui_media_stop 设 g_quit 后线程自身退出,无需 join */
|
||||
if (need_start) pthread_detach(g_thr);
|
||||
}
|
||||
|
||||
void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms,
|
||||
const char *title, const char *artist, const char *album,
|
||||
const char *artwork, const char *lyric)
|
||||
{
|
||||
m_meta_t mm;
|
||||
memset(&mm, 0, sizeof mm);
|
||||
pthread_mutex_lock(&g_mu);
|
||||
media_app_id();
|
||||
snprintf(mm.app_id, sizeof mm.app_id, "%s", g_app);
|
||||
mm.state = state;
|
||||
mm.pos_ms = pos_ms;
|
||||
mm.dur_ms = dur_ms;
|
||||
if (title) snprintf(mm.title, sizeof mm.title, "%s", title);
|
||||
if (artist) snprintf(mm.artist, sizeof mm.artist, "%s", artist);
|
||||
if (album) snprintf(mm.album, sizeof mm.album, "%s", album);
|
||||
if (artwork && artwork[0]) { snprintf(mm.artwork, sizeof mm.artwork, "%s", artwork); mm.flags |= MF_ART | MF_COVER; }
|
||||
if (lyric && lyric[0]) { snprintf(mm.lyric, sizeof mm.lyric, "%s", lyric); mm.flags |= MF_LYRIC; }
|
||||
|
||||
int fd = conn_locked();
|
||||
if (fd >= 0) {
|
||||
if (send_frame(fd, MSG_META, (uint32_t)((uint64_t)pos_ms & 0x7FFFFFFF), &mm, sizeof mm) != 0) {
|
||||
mark_dead_locked(); /* 断线:下次 meta 重连 */
|
||||
fd = conn_locked();
|
||||
if (fd >= 0) (void)send_frame(fd, MSG_META, (uint32_t)((uint64_t)pos_ms & 0x7FFFFFFF), &mm, sizeof mm);
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
void meow_ui_media_stop(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
g_quit = 1;
|
||||
mark_dead_locked();
|
||||
g_thr_on = 0;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
|
||||
/* ---- 物理媒体键 → 系统语义(M2.xx:SDK 输入层接管,App 前台也生效)----
|
||||
* 物理键是系统资源(APPS.md §3.4.1):播放/暂停/上一曲/下一曲 → 转发到当前媒体会话。
|
||||
* 本进程注册过 playctl 回调(= 本 App 是媒体源)→ 直接本地回调(最简,无 IPC 往返);
|
||||
* 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到「最近活跃媒体会话」。best-effort 绝不崩。 */
|
||||
|
||||
/* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停切换)。
|
||||
* 播放/暂停(164)恒发 cmd=6 toggle——SDK 本地不再按「最近上报 state」判 play/pause:
|
||||
* 走自有 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)
|
||||
{
|
||||
switch (code) {
|
||||
case 164: /* KEY_PLAYPAUSE:toggle(源侧按当前态翻转) */
|
||||
return 6;
|
||||
case 165: case 19: /* KEY_PREVIOUSSONG / 旧码 */
|
||||
return 3;
|
||||
case 163: /* KEY_NEXTSONG */
|
||||
return 4;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 控制器路径:发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话(launcher ipc_dispatch
|
||||
* MSG_PLAYCTL → meow_media_playctl → 下发到当前源 fd)。
|
||||
* 专门开一条临时连接(不复用媒体源持久连接 g_fd,避免与后台线程 poll/读竞争);
|
||||
* 无 launcher/断线 → 静默降级(绝不崩)。 */
|
||||
static void send_playctl_upstream(int cmd, int64_t seek_ms)
|
||||
{
|
||||
int fd = connect_path(sock_path());
|
||||
if (fd < 0) return;
|
||||
m_playctl_t c;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.cmd = (uint32_t)cmd;
|
||||
c.seek_ms = seek_ms;
|
||||
if (send_frame(fd, MSG_PLAYCTL, 1, &c, sizeof c) != 0) { close(fd); return; }
|
||||
/* 读 ACK(best-effort):带 200ms poll,防 launcher 挂死导致输入线程阻塞。 */
|
||||
struct pollfd p;
|
||||
p.fd = fd; p.events = POLLIN; p.revents = 0;
|
||||
if (poll(&p, 1, 200) > 0 && (p.revents & POLLIN)) {
|
||||
uint8_t rbuf[128];
|
||||
uint16_t rt;
|
||||
(void)read_frame(fd, &rt, rbuf, sizeof rbuf);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* 物理媒体键按下边沿(val==1 时由输入线程调用;重复/抬起不重复触发)。 */
|
||||
void meow_ui_media_sys_key(int code)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
meow_media_playctl_fn cb = g_cb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
int cmd = media_key_cmd(code);
|
||||
if (cmd <= 0) return;
|
||||
if (cb) { cb(cmd, 0); return; } /* 本 App 是媒体源(注册过回调)→ 本地回调 */
|
||||
send_playctl_upstream(cmd, 0); /* 否则经 IPC 发 launcher 转发到最近活跃会话 */
|
||||
}
|
||||
|
||||
/* 后台线程主体:懒连 + poll 读 launcher 下发 MSG_PLAYCTL → 调 cb。 */
|
||||
static void *media_thread_main(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!g_quit) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int fd = g_fd;
|
||||
if (fd < 0) fd = conn_locked(); /* 懒连/重连(best-effort;失败则回到循环再试) */
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (fd < 0) { usleep(500000); continue; }
|
||||
|
||||
struct pollfd p;
|
||||
p.fd = fd; p.events = POLLIN; p.revents = 0;
|
||||
int pr = poll(&p, 1, 200);
|
||||
if (pr < 0 && errno != EINTR) {
|
||||
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
|
||||
continue;
|
||||
}
|
||||
if (pr > 0 && (p.revents & (POLLHUP | POLLERR | POLLNVAL))) {
|
||||
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
|
||||
continue;
|
||||
}
|
||||
if (pr > 0 && (p.revents & POLLIN)) {
|
||||
uint8_t rbuf[MEOW_IPC_MAX];
|
||||
uint16_t type;
|
||||
int n = read_frame(fd, &type, rbuf, sizeof rbuf);
|
||||
if (n < 0) {
|
||||
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
|
||||
continue;
|
||||
}
|
||||
if (type == MSG_PLAYCTL && n >= (int)sizeof(m_playctl_t)) {
|
||||
m_playctl_t c;
|
||||
memcpy(&c, rbuf, sizeof c);
|
||||
/* g_cb 经互斥读快照后调用(App 回调可能在 UI 线程,简化为直接调) */
|
||||
pthread_mutex_lock(&g_mu);
|
||||
meow_media_playctl_fn cb = g_cb;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
if (cb) cb((int)c.cmd, (int64_t)c.seek_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
pthread_mutex_lock(&g_mu); mark_dead_locked(); pthread_mutex_unlock(&g_mu);
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/* meow_ui_media.h — App 侧媒体会话封装(SDK 公共件;FRAMEWORK_DESIGN 附录 B / D1)。
|
||||
*
|
||||
* 让框架/第三方播放类 App 以 SDK 标准上报实时元数据(MSG_META)并接收控制(MSG_PLAYCTL)。
|
||||
* 声明 MF_PLAYBACK 的 App 首次上报 → launcher 注册为媒体源(最近活跃=当前源);通知中心
|
||||
* 媒体卡片 / 主界面 Now Playing 页据此显示与控制。
|
||||
*
|
||||
* 连接管理(best-effort):懒连(首次 meta/注册回调时 connect)+ 断线重连(下次上报时重试);
|
||||
* launcher 不在 → 优雅静默降级,绝不崩。帧格式/负载复用 launcher 共享 meow_ipc.h(同一份)。
|
||||
*
|
||||
* 命名:launcher 侧已占用 meow_media_*(meow_media.c),SDK 侧统一 meow_ui_media_* 前缀,
|
||||
* 避免两态同符号在 launcher(链 meow_ui.a + meow_media.c)出现重复定义。
|
||||
*/
|
||||
#ifndef MEOW_UI_MEDIA_H
|
||||
#define MEOW_UI_MEDIA_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 播放控制回调:cmd 1=播放 2=暂停 3=上一首 4=下一首 5=seek(seek_ms 为毫秒)6=播放/暂停切换
|
||||
* (toggle:物理播放/暂停键 164 恒发 6,由媒体源按自身当前态翻转——源是状态权威)。 */
|
||||
typedef void (*meow_media_playctl_fn)(int cmd, int64_t seek_ms);
|
||||
|
||||
/* 指定 app_id(NULL=env MEOW_LOG_APP,缺省 "app")。幂等,首次调用生效。 */
|
||||
void meow_ui_media_init(const char *app_id);
|
||||
|
||||
/* 注册播放控制回调并启动后台接收线程(懒起;重复调用幂等)。
|
||||
* 线程:连接(HELLO+MF_PLAYBACK 注册为媒体源)→ poll 读 launcher 下发 MSG_PLAYCTL → 调 cb。 */
|
||||
void meow_ui_media_on_playctl(meow_media_playctl_fn cb);
|
||||
|
||||
/* 上报实时元数据/进度/状态(state:0=停止 1=播放 2=暂停 3=缓冲)。
|
||||
* artwork/lyric 非空 → 自动置 MF_ART/MF_COVER/MF_LYRIC 标志;封面/歌词路径或 MeowOS 槽位。
|
||||
* best-effort:无 socket/launcher 不在 → 静默降级(打日志不崩)。 */
|
||||
void meow_ui_media_meta(uint32_t state, uint32_t pos_ms, uint32_t dur_ms,
|
||||
const char *title, const char *artist, const char *album,
|
||||
const char *artwork, const char *lyric);
|
||||
|
||||
/* 停止后台接收线程并断开连接(App 退出时调;幂等)。 */
|
||||
void meow_ui_media_stop(void);
|
||||
|
||||
/* 物理媒体键 → 系统语义(SDK 输入层调用;App 前台也生效)。
|
||||
* code:164=播放/暂停(恒发 cmd=6 toggle,源侧翻转)、165/19=上一曲、163=下一曲。
|
||||
* 本 App 注册过 playctl 回调(媒体源)→ 本地直接回调 cb(cmd,seek_ms);
|
||||
* 否则经 IPC 发 MSG_PLAYCTL 给 launcher 转发到最近活跃媒体会话。非媒体键/无源静默。
|
||||
* best-effort:无 launcher/socket → 优雅降级,绝不崩。 */
|
||||
void meow_ui_media_sys_key(int code);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_MEDIA_H */
|
||||
@@ -0,0 +1,74 @@
|
||||
/* meow_ui_nav.c — App 侧 M3 导航上报封装(SDK 公共件;见 meow_ui_nav.h)。
|
||||
*
|
||||
* 只做「组装 m_nav_t + 经 meow_notify_ipc 一次性发送 MSG_NAV」,不维护连接/订阅循环;
|
||||
* launcher 主循环单写者消费(回复 ACK)。全部失败路径优雅降级(无 socket/写失败/超时
|
||||
* → 静默返回,不崩)。状态(栈深/顶层/app_id)仅进程内缓存,无并发写(App 页面线程调用)。
|
||||
*/
|
||||
#include "meow_ui_nav.h"
|
||||
#include "meow_ipc.h"
|
||||
#include "meow_notify_ipc.h" /* meow_notify_ipc_send_raw(复用其一次性帧传输层) */
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static char g_app_id[MEOW_APP_ID_MAX] = "";
|
||||
static int g_depth = 0;
|
||||
static char g_top[MEOW_ACT_MAX] = "";
|
||||
static int g_inited = 0;
|
||||
|
||||
/* app_id:env MEOW_LOG_APP(与 meow_ui_log/meow_notify_ipc 取值口径一致),缺省 "app"。 */
|
||||
static const char *nav_app_id(void)
|
||||
{
|
||||
if (!g_app_id[0]) {
|
||||
const char *s = getenv("MEOW_LOG_APP");
|
||||
snprintf(g_app_id, sizeof g_app_id, "%s", (s && s[0]) ? s : "app");
|
||||
}
|
||||
return g_app_id;
|
||||
}
|
||||
|
||||
int meow_ui_nav_init(void)
|
||||
{
|
||||
if (g_inited) return g_depth;
|
||||
g_inited = 1;
|
||||
nav_app_id();
|
||||
const char *chain = getenv("MEOW_CHAIN");
|
||||
if (chain && chain[0]) {
|
||||
g_depth = 1;
|
||||
for (const char *p = chain; *p; p++) if (*p == ';') g_depth++;
|
||||
}
|
||||
const char *act = getenv("MEOW_ACTIVITY");
|
||||
if (act && act[0]) snprintf(g_top, sizeof g_top, "%s", act);
|
||||
return g_depth;
|
||||
}
|
||||
|
||||
int meow_ui_nav_depth(void) { meow_ui_nav_init(); return g_depth; }
|
||||
const char *meow_ui_nav_top(void) { meow_ui_nav_init(); return g_top; }
|
||||
|
||||
void meow_ui_nav_report(uint32_t depth, const char *top)
|
||||
{
|
||||
meow_ui_nav_init();
|
||||
m_nav_t n;
|
||||
memset(&n, 0, sizeof n);
|
||||
snprintf(n.app_id, sizeof n.app_id, "%s", nav_app_id());
|
||||
n.nav_type = MEOW_NAV_REPORT;
|
||||
n.depth = depth;
|
||||
if (top) snprintf(n.current, sizeof n.current, "%s", top);
|
||||
(void)meow_notify_ipc_send_raw(MSG_NAV, &n, (uint32_t)sizeof n, 1);
|
||||
/* 进程内镜像:App 侧也维持当前栈深/顶层(跨 TU 只读用) */
|
||||
g_depth = (int)depth;
|
||||
if (top) snprintf(g_top, sizeof g_top, "%s", top);
|
||||
}
|
||||
|
||||
void meow_ui_nav_request_exit(void)
|
||||
{
|
||||
meow_ui_nav_init();
|
||||
m_nav_t n;
|
||||
memset(&n, 0, sizeof n);
|
||||
snprintf(n.app_id, sizeof n.app_id, "%s", nav_app_id());
|
||||
n.nav_type = MEOW_NAV_EXIT;
|
||||
n.depth = 0;
|
||||
(void)meow_notify_ipc_send_raw(MSG_NAV, &n, (uint32_t)sizeof n, 1);
|
||||
g_depth = 0;
|
||||
g_top[0] = 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/* meow_ui_nav.h — App 侧 M3 导航上报封装(SDK 公共件)。
|
||||
*
|
||||
* App 用它把「应用内页面栈深/顶层 activity」经 IPC 报给 launcher(MSG_NAV REPORT),
|
||||
* 以及在根屏按返回时请求退出(MSG_NAV EXIT);launcher 据此维护每 app 导航栈(纯上报,
|
||||
* 回退边界由 launcher 统一决定)。App 不渲染系统 UI(FRAMEWORK_DESIGN §3 / MEOW_APP_SDK §6.1)。
|
||||
*
|
||||
* 命名:launcher 侧已占用 meow_nav_*(app_launcher.c),SDK 侧统一 meow_ui_nav_* 前缀,
|
||||
* 避免两态同符号在 launcher(链 meow_ui.a + app_launcher.c)出现重复定义。
|
||||
*
|
||||
* 冷启动链路:launcher 经 MEOW_ACTIVITY=<top> / MEOW_CHAIN=<id0>;<id1>;...;<top> 注入;
|
||||
* meow_ui_nav_init() 读之初始化栈深/顶层。App 自行 push/pop 页面后经 meow_ui_nav_report()
|
||||
* 上报新深度/顶层。
|
||||
*/
|
||||
#ifndef MEOW_UI_NAV_H
|
||||
#define MEOW_UI_NAV_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 从 env 初始化(幂等):MEOW_CHAIN 段数→栈深;MEOW_ACTIVITY→顶层 activity_id。
|
||||
* 未注入(裸启动)→ 深度 0、顶层空串。app_id 取 env MEOW_LOG_APP(缺省 "app")。
|
||||
* 返回当前逻辑深度(0=未注入/裸启动)。 */
|
||||
int meow_ui_nav_init(void);
|
||||
|
||||
/* 当前栈深 / 顶层 activity_id(meow_ui_nav_init 后为其初值;App 上报后更新)。 */
|
||||
int meow_ui_nav_depth(void);
|
||||
const char *meow_ui_nav_top(void);
|
||||
|
||||
/* 上报 MEOW_NAV_REPORT:App 弹/推自己的屏后调用(depth=当前逻辑深度 1..16,top=顶层)。
|
||||
* best-effort:launcher 不在/无 socket 静默降级,不崩。 */
|
||||
void meow_ui_nav_report(uint32_t depth, const char *top);
|
||||
|
||||
/* 上报 MEOW_NAV_EXIT:App 在根屏按返回,请求 launcher 终止应用回主屏。best-effort。 */
|
||||
void meow_ui_nav_request_exit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_NAV_H */
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* meow_ui_notify.c — MeowOS 应用通知 API(meow_notify_*,M2.53 最小可用版)。
|
||||
*
|
||||
* 定位:app 只需声明类型 + meow_notify_send() 即可发通知,SDK 管配额/活动表/降级。
|
||||
* 本切片为「SDK 侧应用可调用 API」第一个纵向切片——只做 SDK 层;通知中心 UI、
|
||||
* D-Bus 守护(org.freedesktop.Notifications)、TF 转储是后续独立任务。
|
||||
*
|
||||
* 行为约定(NOTIFICATION_SYSTEM.md §2/§7,2026-08-29 用户拍板):
|
||||
* - 类型注册:app 声明支持的类型,最多 MEOW_NOTIFY_MAX_TYPES(4)种;
|
||||
* meow_notify_send 也会自动注册未声明类型(受同样 4 种上限约束)。
|
||||
* - 配额:每类型活动(未过期)通知上限 MEOW_NOTIFY_MAX_PER_TYPE(4),满则丢最旧
|
||||
* (替换);媒体类型单例(每 app 仅保留最近 1 条,重发替换——可多媒体 App)。
|
||||
* - 超时语义 [实现定案]:timeout_ms > 0 为该毫秒数后过期;0 = 不自动过期(直到
|
||||
* meow_notify_cancel);-1 = 常驻(ONGOING),同样不自动过期。
|
||||
* - 落点(任务 #5 SDK↔IPC 桥):本表登记后,经 IPC 把 MSG_NOTIFY(0x0020) 送到
|
||||
* launcher 中央存储(meow_notif.c 的 meow_notif_post),回读 MSG_NOTIFY_ACK(0x0021)
|
||||
* 取 launcher 分配的通知 id(nid,存进 slot->ipc_id,供后续 replaces_id/dismiss 用)。
|
||||
* 通知中心 UI / D-Bus / TF 转储由 launcher 侧处理;SDK 本地活动表保留(app 进程内
|
||||
* 即时查询/配额自检)。无 socket/写失败/超时 → 优雅降级(保留本地 id 兜底,不崩)。
|
||||
* meow_notify 结构已对齐 freedesktop 字段(summary/body/…)为此预留。
|
||||
*
|
||||
* 线程安全:pthread_mutex(同 meow_ui_log.c,launcher 前后台进程都会在已链 -lpthread
|
||||
* 的应用里调用),且 IPC 发送/读 ACK 在持锁之外进行(避免 socket I/O 阻塞其它查询)。
|
||||
* 不依赖 /dev/fb0、不依赖板子,可宿主编译。内部只复用 meow_ui_time_ms()(单调毫秒)、
|
||||
* meow_log()(统一日志)与 meow_notify_ipc_*(IPC 桥,纯传输层)三个既有/新增 SDK 能力。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_notify_ipc.h" /* 任务 #5:通知 → launcher 中央存储的 IPC 桥(映射+传输) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ---- 内部活动表尺寸(配额见 meow_ui.h) ---- */
|
||||
#define MEOW_NOTIFY_MAX_ACTIVE (MEOW_NOTIFY_MAX_TYPES * MEOW_NOTIFY_MAX_PER_TYPE)
|
||||
|
||||
/* 活动槽内 icon 缓冲上限(与 IPC app_icon[48] 一致;SDK 复制副本,不持 app 外部指针) */
|
||||
#define MEOW_SLOT_ICON_MAX 48
|
||||
|
||||
/* 活动通知槽(内部;标题/正文/图标等复制副本,不持 app 外部指针,避免栈字符串悬垂) */
|
||||
typedef struct {
|
||||
uint32_t id; /* 本地唯一 id(app 侧句柄);0=空闲槽 */
|
||||
uint32_t ipc_id; /* launcher 中央存储分配的通知 id(nid);0=未同步/降级 */
|
||||
int active; /* 1=活动(未取消未过期),0=空闲 */
|
||||
meow_notify_category cat;
|
||||
meow_notify_urgency urg;
|
||||
int timeout_ms;
|
||||
uint64_t created_ms;
|
||||
char title[64];
|
||||
char body[128];
|
||||
char target_activity[32];
|
||||
char icon[MEOW_SLOT_ICON_MAX]; /* 图标 name/path;空=用 app 默认图标(launcher 回退) */
|
||||
int progress; /* 0-100;-1=无进度条 */
|
||||
} notify_slot_t;
|
||||
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static notify_slot_t g_slots[MEOW_NOTIFY_MAX_ACTIVE];
|
||||
static int g_cats[MEOW_NOTIFY_MAX_TYPES]; /* 已声明类型(按注册顺序) */
|
||||
static int g_cat_n = 0;
|
||||
static uint32_t g_next_id = 1;
|
||||
|
||||
/* 字符串表(与枚举序号对齐) */
|
||||
static const char *g_urg_str[] = { "low", "normal", "high", "critical" };
|
||||
static const char *g_cat_str[] = { "general", "message", "email", "event", "progress", "media" };
|
||||
|
||||
/* ---- 内部工具(以下均须持有 g_mu 时调用) ---- */
|
||||
|
||||
/* 该分类当前活动数。 */
|
||||
static int notify_count_cat(meow_notify_category cat)
|
||||
{
|
||||
int i, n = 0;
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (g_slots[i].active && g_slots[i].cat == cat) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 该分类是否已被 app 声明。 */
|
||||
static int notify_is_declared(meow_notify_category cat)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < g_cat_n; i++)
|
||||
if (g_cats[i] == (int)cat) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 清掉已过期活动槽(TTL 用 meow_ui_time_ms() 单调毫秒)。 */
|
||||
static void notify_purge_expired(void)
|
||||
{
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
int i;
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) {
|
||||
notify_slot_t *s = &g_slots[i];
|
||||
if (!s->active) continue;
|
||||
if (s->timeout_ms > 0 && (now - s->created_ms) >= (uint64_t)s->timeout_ms) {
|
||||
s->active = 0;
|
||||
s->id = 0;
|
||||
s->ipc_id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 填充一个活动槽(改前须确认槽空闲或已被显式复用)。 */
|
||||
static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uint64_t now)
|
||||
{
|
||||
s->id = id;
|
||||
s->ipc_id = 0; /* 初值=未同步;发送后由 notify_deliver 回填 launcher 分配的 nid */
|
||||
s->active = 1;
|
||||
s->cat = n->cat;
|
||||
s->urg = ((int)n->urgency >= 0 && (int)n->urgency < (int)MEOW_NOTIFY_URG_COUNT)
|
||||
? n->urgency : MEOW_NOTIFY_URG_NORMAL;
|
||||
s->timeout_ms = n->timeout_ms;
|
||||
s->created_ms = now;
|
||||
snprintf(s->title, sizeof s->title, "%s", n->title ? n->title : "");
|
||||
snprintf(s->body, sizeof s->body, "%s", n->body ? n->body : "");
|
||||
snprintf(s->target_activity, sizeof s->target_activity, "%s",
|
||||
n->target_activity ? n->target_activity : "");
|
||||
snprintf(s->icon, sizeof s->icon, "%s", n->icon ? n->icon : "");
|
||||
s->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress);
|
||||
}
|
||||
|
||||
/* 落点(任务 #5):把通知经 IPC MSG_NOTIFY(0x0020) 送到 launcher 中央存储
|
||||
* (meow_notif.c 的 meow_notif_post),回读 MSG_NOTIFY_ACK(0x0021) 取 launcher 分配的
|
||||
* 通知 id(nid)。返回 nid(>0=成功);无 socket/写失败/超时 → 返回 0(调用方保留本地 id
|
||||
* 兜底,不崩)。调用方须在持锁外调用(socket I/O 不占 g_mu,避免阻塞其它查询)。
|
||||
* replaces_id:0=新建;>0=更新 launcher 侧已存在通知(freedesktop 语义)。 */
|
||||
static uint32_t notify_deliver(const meow_notify *n, uint32_t replaces_id)
|
||||
{
|
||||
uint32_t nid = meow_notify_ipc_send(n, replaces_id);
|
||||
if (nid) {
|
||||
meow_log(MEOW_LOG_INFO, "[notify] delivered nid=%u replaces=%u cat=%s title=%s",
|
||||
(unsigned)nid, (unsigned)replaces_id, meow_notify_category_str(n->cat),
|
||||
n->title ? n->title : "");
|
||||
} else {
|
||||
meow_log(MEOW_LOG_INFO, "[notify] degraded (no launcher) cat=%s title=%s",
|
||||
meow_notify_category_str(n->cat), n->title ? n->title : "");
|
||||
}
|
||||
return nid;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 公共 API(见 meow_ui.h §9.1) */
|
||||
/* ============================================================ */
|
||||
|
||||
int meow_notify_register_type(meow_notify_category cat)
|
||||
{
|
||||
int rc = 0;
|
||||
if ((int)cat < 0 || (int)cat >= (int)MEOW_NOTIFY_CAT_COUNT) return -1;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (notify_is_declared(cat)) rc = -1; /* 重复声明 */
|
||||
else if (g_cat_n >= MEOW_NOTIFY_MAX_TYPES) rc = -1; /* 超 4 种上限 */
|
||||
else g_cats[g_cat_n++] = (int)cat;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
uint32_t meow_notify_send(const meow_notify *n)
|
||||
{
|
||||
uint32_t id, nid, replaces_id = 0;
|
||||
uint64_t now, oldest;
|
||||
int i, slot_idx, count, oldest_idx;
|
||||
|
||||
if (!n || (int)n->cat < 0 || (int)n->cat >= (int)MEOW_NOTIFY_CAT_COUNT) return 0;
|
||||
if (!n->title) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_mu);
|
||||
notify_purge_expired();
|
||||
|
||||
/* 未声明类型:自动注册(≤4 种,超过则拒绝本次发送) */
|
||||
if (!notify_is_declared(n->cat)) {
|
||||
if (g_cat_n >= MEOW_NOTIFY_MAX_TYPES) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
g_cats[g_cat_n++] = (int)n->cat;
|
||||
}
|
||||
|
||||
slot_idx = -1;
|
||||
if (n->cat == MEOW_NOTIFY_CAT_MEDIA) {
|
||||
/* 媒体单例:复用已存在的媒体槽(重发替换同 app 上一条媒体通知) */
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (g_slots[i].active && g_slots[i].cat == MEOW_NOTIFY_CAT_MEDIA) {
|
||||
slot_idx = i;
|
||||
replaces_id = g_slots[i].ipc_id; /* 更新该媒体通知(0=降级/未同步) */
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
count = notify_count_cat(n->cat);
|
||||
if (count >= MEOW_NOTIFY_MAX_PER_TYPE) {
|
||||
/* 满则丢该类型最旧一条(更新):先取被丢最旧的 launcher nid 作 replaces_id,
|
||||
* 再释放槽(顺序不可反,否则 ipc_id 已被清 0)。 */
|
||||
oldest_idx = -1;
|
||||
oldest = UINT64_MAX;
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (g_slots[i].active && g_slots[i].cat == n->cat && g_slots[i].created_ms < oldest) {
|
||||
oldest = g_slots[i].created_ms;
|
||||
oldest_idx = i;
|
||||
}
|
||||
if (oldest_idx >= 0) {
|
||||
replaces_id = g_slots[oldest_idx].ipc_id; /* 复用被丢最旧的 launcher nid */
|
||||
if (replaces_id == 0) replaces_id = 0; /* 未同步时按新建处理 */
|
||||
g_slots[oldest_idx].active = 0;
|
||||
g_slots[oldest_idx].id = 0;
|
||||
g_slots[oldest_idx].ipc_id = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slot_idx < 0) {
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (!g_slots[i].active) { slot_idx = i; break; }
|
||||
}
|
||||
if (slot_idx < 0) {
|
||||
/* 理论不可达(上方已丢最旧腾位);兜底防御 */
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return 0;
|
||||
}
|
||||
|
||||
now = meow_ui_time_ms();
|
||||
id = g_next_id++;
|
||||
if (g_next_id == 0) g_next_id = 1; /* 防回绕 */
|
||||
notify_fill(&g_slots[slot_idx], n, id, now);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
|
||||
/* 落点(持锁外):IPC 送 launcher 中央存储,回读 nid;无 launcher 则降级(nid=0)。 */
|
||||
nid = notify_deliver(n, replaces_id);
|
||||
|
||||
/* 回填 launcher 分配的 nid(槽可能已被并发 cancel/过期清掉——仅在时回填)。 */
|
||||
if (nid) {
|
||||
pthread_mutex_lock(&g_mu);
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (g_slots[i].active && g_slots[i].id == id) { g_slots[i].ipc_id = nid; break; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
int meow_notify_cancel(uint32_t id)
|
||||
{
|
||||
int i, rc = 0;
|
||||
uint32_t ipc_id = 0;
|
||||
if (id == 0) return 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
notify_purge_expired();
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) {
|
||||
if (g_slots[i].active && g_slots[i].id == id) {
|
||||
ipc_id = g_slots[i].ipc_id; /* launcher 侧 nid;0=未同步/降级 */
|
||||
g_slots[i].active = 0;
|
||||
g_slots[i].id = 0;
|
||||
g_slots[i].ipc_id = 0;
|
||||
rc = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
/* 落点:经 IPC MSG_NOTIFY_DISMISS(0x0022) 撤下 launcher 中央存储对应通知
|
||||
* (best-effort;无 launcher/未同步则静默降级)。 */
|
||||
if (rc && ipc_id) meow_notify_ipc_dismiss(ipc_id);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int meow_notify_update_progress(uint32_t local_id, int progress)
|
||||
{
|
||||
int p, i, found = 0;
|
||||
uint32_t ipc_id = 0;
|
||||
if (local_id == 0) return 0;
|
||||
p = (progress < -1) ? -1 : (progress > 100 ? 100 : progress); /* 钳位 [-1,100] */
|
||||
pthread_mutex_lock(&g_mu);
|
||||
notify_purge_expired();
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++) {
|
||||
if (g_slots[i].active && g_slots[i].id == local_id) {
|
||||
g_slots[i].progress = p; /* 实时:仅改内存活动槽,不落盘 */
|
||||
ipc_id = g_slots[i].ipc_id; /* launcher 侧 nid;0=未同步/降级 */
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
/* 落点(best-effort):经 IPC 通知 launcher 改其内存活跃槽;无 launcher/未同步则静默。 */
|
||||
if (found && ipc_id) meow_notify_ipc_update_progress(ipc_id, p);
|
||||
return found;
|
||||
}
|
||||
|
||||
int meow_notify_active_count(void)
|
||||
{
|
||||
int i, n = 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
notify_purge_expired();
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (g_slots[i].active) n++;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return n;
|
||||
}
|
||||
|
||||
int meow_notify_count_in_type(meow_notify_category cat)
|
||||
{
|
||||
int n;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
notify_purge_expired();
|
||||
n = notify_count_cat(cat);
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return n;
|
||||
}
|
||||
|
||||
int meow_notify_has_active(void)
|
||||
{
|
||||
int i, rc = 0;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
notify_purge_expired();
|
||||
for (i = 0; i < MEOW_NOTIFY_MAX_ACTIVE; i++)
|
||||
if (g_slots[i].active) { rc = 1; break; }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
const char *meow_notify_urgency_str(meow_notify_urgency u)
|
||||
{
|
||||
/* 钳位(同 meow_ui_log.c 防越界枚举触发 OOB 读) */
|
||||
if ((int)u >= 0 && (int)u < (int)MEOW_NOTIFY_URG_COUNT) return g_urg_str[u];
|
||||
return g_urg_str[MEOW_NOTIFY_URG_NORMAL];
|
||||
}
|
||||
|
||||
const char *meow_notify_category_str(meow_notify_category c)
|
||||
{
|
||||
if ((int)c >= 0 && (int)c < (int)MEOW_NOTIFY_CAT_COUNT) return g_cat_str[c];
|
||||
return g_cat_str[MEOW_NOTIFY_CAT_GENERAL];
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
/*
|
||||
* meow_ui_sys.c — SDK 内置系统状态读取器 + 全局状态栏(A-0 核心)。
|
||||
*
|
||||
* 职责:
|
||||
* 1. 状态读取器:读 /sys/class/power_supply/(电量/充电)、WiFi operstate、
|
||||
* 蓝牙实上电、音量(/usr/data/meow/settings.conf 的 audio.volume,与 launcher
|
||||
* 同源单点);时钟在绘制时现取 localtime。1s 轻刷,蓝牙 30s 缓存。
|
||||
* 2. 全局状态栏绘制:把时钟/电量/WiFi/蓝牙/音量画进 back_buf 顶部 MUI_SBAR_H 行;
|
||||
* 由渲染后端 present() 前置调用(z 最顶),非全屏可见。
|
||||
* 3. 显隐/全屏状态 + 公共 API(meow_ui_set_statusbar / set_fullscreen / ...)。
|
||||
*
|
||||
* 注意:所有读取在事件循环主线内做(无独立线程),1s 频率同步读取 sysfs/小文件,
|
||||
* 开销可忽略;蓝牙 hciconfig 为 popen(spawn) 较贵,仅 30s 刷一次并缓存。
|
||||
*/
|
||||
#include "meow_ui_sys.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_notify_ipc.h" /* meow_notify_ipc_notif_open:App 前台 #10.5 上报 */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <time.h>
|
||||
|
||||
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||||
|
||||
/* App 前台顶条模式文件(App 进程 SDK 写,launcher 读)。与 /tmp/meowapp_state(immersive,launcher 写/
|
||||
* watchdog 读)语义不同,独立文件避免冲突。值:'0'=展开 28px 整条;'1'=收合(提示线);'2'=隐藏(沉浸 inset0)。
|
||||
* 仅 App 进程写(!g_sys_render);launcher(系统 UI)不写,只读。App 退出时由 launcher unlink。 */
|
||||
#define MEOW_SBAR_MODE_FILE "/tmp/meowapp_sbar"
|
||||
|
||||
/* 蓝牙 powered 缓存刷新间隔(hciconfig 较贵) */
|
||||
#define BT_REFRESH_MS 30000
|
||||
|
||||
static meow_sys_state g_sys;
|
||||
static int g_sys_inited = 0;
|
||||
|
||||
/* 显隐/全屏状态(公共 API 与绘制/输入共用) */
|
||||
static int g_statusbar_on = 1; /* 默认显示 */
|
||||
static int g_fullscreen = 0; /* 默认非全屏 */
|
||||
static int g_statusbar_force = 0; /* 1s 刷新后置位,强制重画顶部条 */
|
||||
/* 「同一个状态栏」闭环(§3.1/FRAMEWORK_DESIGN §10.2):状态栏 = 系统级组件,仅系统进程
|
||||
* (launcher) 渲染。g_sys_render=1 表示本进程是系统 UI —— meow_ui_sys_statusbar_render /
|
||||
* present() 据此决定是否画出顶条/提示线;App 进程保持 0(不渲染)+ 仍 inset 28(launcher 叠加)。 */
|
||||
static int g_sys_render = 0; /* 1=本进程为系统 UI(渲染);0=App(不渲染,仅 inset) */
|
||||
|
||||
static uint64_t now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
/* 读一份 sysfs/文件字符串(剥尾随换行);失败回退 "" */
|
||||
static void read_str(const char *path, char *buf, size_t n)
|
||||
{
|
||||
buf[0] = 0;
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return;
|
||||
ssize_t r = read(fd, buf, n - 1);
|
||||
close(fd);
|
||||
if (r <= 0) { buf[0] = 0; return; }
|
||||
buf[r] = 0;
|
||||
while (r > 0 && (buf[r-1] == '\n' || buf[r-1] == '\r')) buf[--r] = 0;
|
||||
}
|
||||
|
||||
static int read_int(const char *path)
|
||||
{
|
||||
char buf[24];
|
||||
read_str(path, buf, sizeof buf);
|
||||
if (!buf[0]) return -1;
|
||||
return atoi(buf);
|
||||
}
|
||||
|
||||
static int hci_powered(void)
|
||||
{
|
||||
FILE *p = popen("hciconfig -a hci0 2>/dev/null", "r");
|
||||
if (!p) return 0;
|
||||
int up = 0;
|
||||
char buf[256];
|
||||
while (fgets(buf, sizeof buf, p)) {
|
||||
char *s = buf, *tok;
|
||||
while ((tok = strsep(&s, " \t\r\n")) != NULL) {
|
||||
if (!strcmp(tok, "UP")) { up = 1; break; }
|
||||
}
|
||||
}
|
||||
pclose(p);
|
||||
return up;
|
||||
}
|
||||
|
||||
/* 音量:读 settings.conf 的 audio.volume(launcher 同源,watchdog 写同文件)。
|
||||
* 与 launcher 口径一致:0-100,且尊重 audio.volume_max 上限 clamp(vol_clamp 同义)。
|
||||
* 注意 settings 落盘为 "key = value"(带空格),必须按 '=' 切分解析,不能朴素前缀匹配——
|
||||
* 否则永远读不到(回退默认 60,即「状态栏音量数字不对、卡在 60」的根因)。 */
|
||||
static int settings_vol(void)
|
||||
{
|
||||
int vol = meow_ui_vol_read(MEOW_SETTINGS_FILE, "audio.volume", MEOW_UI_VOL_DEFAULT);
|
||||
int vol_max = meow_ui_vol_read(MEOW_SETTINGS_FILE, "audio.volume_max", MEOW_UI_VOL_MAX_DEF);
|
||||
return meow_ui_vol_clamp(vol, vol_max);
|
||||
}
|
||||
|
||||
/* 亮度:读 settings.conf 的 display.brightness(launcher 同源),并 clamp 到
|
||||
* [display.brightness_min, 100](与 launcher clamp_brightness 同口径,下限默认 1=极暗非全黑)。
|
||||
* Stage-One 状态栏收敛(launcher 退役自绘状态栏)后,launcher 侧用 meow_ui_sys_feed() 喂
|
||||
* 即时值(写入方=launcher);此处兜底让 App(meow_ui_run 的 1s 轻刷)也读到并显示亮度,
|
||||
* 保证「launcher / App 同一实现、同日/夜」。 */
|
||||
static int settings_bright(void)
|
||||
{
|
||||
int v = meow_ui_vol_read(MEOW_SETTINGS_FILE, "display.brightness", 101);
|
||||
int mn = meow_ui_vol_read(MEOW_SETTINGS_FILE, "display.brightness_min", 1);
|
||||
if (v < mn) v = mn;
|
||||
if (v > 100) v = 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
int meow_ui_sys_init(void)
|
||||
{
|
||||
memset(&g_sys, 0, sizeof g_sys);
|
||||
g_sys.batt = -1;
|
||||
g_sys.vol = settings_vol();
|
||||
g_sys.brightness = settings_bright();
|
||||
g_statusbar_on = 1;
|
||||
g_fullscreen = 0;
|
||||
g_statusbar_force = 1;
|
||||
meow_ui_sys_refresh();
|
||||
g_sys_inited = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void meow_ui_sys_refresh(void)
|
||||
{
|
||||
g_sys.batt = read_int("/sys/class/power_supply/battery/capacity");
|
||||
if (g_sys.batt < 0 || g_sys.batt > 100) g_sys.batt = -1;
|
||||
char st[16];
|
||||
/* 充电态权威源(真机实测 2026-09-01:battery/status 恒 Discharging 撒谎):
|
||||
* 1) mp2731-charger/status(Charging/Full=充电中);2) 回退 fusb302-typec-source/online==1
|
||||
* (USB-C PD 插线);3) 再回退旧逻辑 battery/status==Charging || ac/online==1。 */
|
||||
read_str("/sys/class/power_supply/mp2731-charger/status", st, sizeof st);
|
||||
if (st[0]) {
|
||||
g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0;
|
||||
} else {
|
||||
int onl = read_int("/sys/class/power_supply/fusb302-typec-source/online");
|
||||
if (onl >= 0) {
|
||||
g_sys.charging = (onl == 1) ? 1 : 0;
|
||||
} else {
|
||||
read_str("/sys/class/power_supply/battery/status", st, sizeof st);
|
||||
g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0;
|
||||
if (!g_sys.charging && read_int("/sys/class/power_supply/ac/online") == 1)
|
||||
g_sys.charging = 1;
|
||||
}
|
||||
}
|
||||
|
||||
char op[16];
|
||||
read_str("/sys/class/net/wlan0/operstate", op, sizeof op);
|
||||
g_sys.wifi_connected = !strcmp(op, "up") ? 1 : 0;
|
||||
g_sys.wifi_on = g_sys.wifi_connected; /* SDK 自身读不到射频 vs 关联的区分,一起走 operstate */
|
||||
g_sys.wifi_level = -1000; /* 信号分档由 launcher 喂(唯一权威源);SDK 不读 /proc 无线 */
|
||||
|
||||
g_sys.vol = settings_vol();
|
||||
g_sys.brightness = settings_bright(); /* 兜底读 settings;launcher 喂入会覆盖(更即时) */
|
||||
|
||||
/* 蓝牙:hci0 存在性检查便宜;上电状态贵(hciconfig spawn),30s 缓存。 */
|
||||
static uint64_t last_bt_ms = 0;
|
||||
uint64_t n = now_ms();
|
||||
g_sys.bt_seen = access("/sys/class/bluetooth/hci0", F_OK) == 0;
|
||||
if (!g_sys.bt_seen) {
|
||||
g_sys.bt_on = 0;
|
||||
last_bt_ms = n;
|
||||
} else if (!last_bt_ms || (n - last_bt_ms) >= BT_REFRESH_MS) {
|
||||
g_sys.bt_on = hci_powered();
|
||||
last_bt_ms = n;
|
||||
}
|
||||
|
||||
/* 时钟每秒变 → 强制顶条重画(时间在绘制时现取) */
|
||||
g_statusbar_force = 1;
|
||||
}
|
||||
|
||||
const meow_sys_state *meow_ui_sys_get(void) { return &g_sys; }
|
||||
|
||||
/* ---- 系统进程(launcher)喂入权威快照(Stage-One 状态栏收敛)----
|
||||
* launcher 不跑 SDK 事件循环,其 g_sys(meow_sys_refresh/light + 事件路径维护)是权威数据源
|
||||
* (尤其亮度/音量,写入方=launcher)。此处把它覆盖进 SDK 快照并置 force,驱动下一次
|
||||
* present() 重画顶条。不喂的字段保持 SDK 现有值(由 meow_ui_sys_refresh 同源兜底)。 */
|
||||
void meow_ui_sys_feed(const meow_ui_feed *f)
|
||||
{
|
||||
if (!f) return;
|
||||
g_sys.batt = f->batt;
|
||||
g_sys.charging = f->charging;
|
||||
g_sys.wifi_on = f->wifi_on;
|
||||
g_sys.wifi_connected = f->wifi_connected;
|
||||
g_sys.wifi_level = f->wifi_level;
|
||||
g_sys.bt_seen = f->bt_seen;
|
||||
g_sys.bt_on = f->bt_on;
|
||||
g_sys.vol = f->vol;
|
||||
if (f->brightness <= 0) g_sys.brightness = 0; /* 未知/屏关:不入顶条(>0 才显示) */
|
||||
else if (f->brightness > 100) g_sys.brightness = 100;
|
||||
else g_sys.brightness = f->brightness;
|
||||
g_statusbar_force = 1; /* 数据已变 → 顶条必重画 */
|
||||
}
|
||||
|
||||
/* ---- 系统 UI 模式(「同一个状态栏」闭环;launcher 调用)----
|
||||
* launcher(系统进程)链 meow_ui.a 后调 meow_ui_sys_set_system_mode(1):本进程渲染全局状态栏
|
||||
* (present() 前置合成 + App 前台叠加)。App 进程默认 0:不渲染状态栏、只 inset(§6.1 ③ /
|
||||
* FRAMEWORK_DESIGN §10.2)。meow_ui_sys_statusbar_active()/meow_ui_statusbar_height() 不随此
|
||||
* 开关变(App 仍报告 28px inset + 顶部热区),仅渲染路径受控。 */
|
||||
void meow_ui_sys_set_system_mode(int on)
|
||||
{
|
||||
g_sys_render = on ? 1 : 0;
|
||||
g_statusbar_force = 1; /* 切换即强制下次 present 反映(画/不画顶条) */
|
||||
}
|
||||
|
||||
/* ---- App 前台顶条模式上报(agent-146 全屏收合接入)----
|
||||
* 仅在 App 进程(!g_sys_render)写;launcher(system_mode=1)不写,避免覆盖 App 的状态。
|
||||
* 由 meow_ui_set_fullscreen / meow_ui_statusbar_set_collapsed 调用,使 launcher(read 进程)
|
||||
* 能感知 App 运行时的全屏/收合态,从而把新顶条切到收合(提示线)而非仍画 28px 整条。 */
|
||||
void meow_ui_sbar_write_mode(void)
|
||||
{
|
||||
if (g_sys_render) return; /* 系统进程(launcher)不写 */
|
||||
char m = meow_ui_statusbar_collapsed() ? '1'
|
||||
: (g_fullscreen ? '2' : '0');
|
||||
int fd = open(MEOW_SBAR_MODE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) return;
|
||||
if (write(fd, &m, 1) < 0) { /* ignore write error */ }
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int meow_ui_sys_render_enabled(void)
|
||||
{
|
||||
return g_sys_render;
|
||||
}
|
||||
|
||||
/* ---- 公共 API ---- */
|
||||
void meow_ui_set_statusbar(int visible)
|
||||
{
|
||||
g_statusbar_on = visible ? 1 : 0;
|
||||
g_statusbar_force = 1; /* 显隐切换也要重画顶条 */
|
||||
meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合) */
|
||||
}
|
||||
|
||||
int meow_ui_statusbar_visible(void)
|
||||
{
|
||||
return g_statusbar_on && !g_fullscreen;
|
||||
}
|
||||
|
||||
void meow_ui_set_fullscreen(int on)
|
||||
{
|
||||
g_fullscreen = on ? 1 : 0;
|
||||
g_statusbar_force = 1; /* 进出全屏立即反映(显示/隐藏顶条) */
|
||||
meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合) */
|
||||
}
|
||||
|
||||
int meow_ui_statusbar_height(void)
|
||||
{
|
||||
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)
|
||||
{
|
||||
/* 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)
|
||||
{
|
||||
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(int origin_y)
|
||||
{
|
||||
/* 系统级「下拉展开通知中心」手势接线(FRAMEWORK_DESIGN §10.5):输入层识别出「起始于
|
||||
* 顶部热区的 SWIPE_DOWN」后路由到此。App 进程只做检测+上报(不发事件给 on_swipe),
|
||||
* 通知中心仍由 launcher 渲染——此处经 IPC 发 MSG_NOTIF_OPEN 请求 launcher 展开叠加会话
|
||||
* (SIGSTOP App + 读回其帧 + 叠加渲染通知中心)。best-effort:无 socket/launcher 不在
|
||||
* → meow_notify_ipc_notif_open 静默降级。 */
|
||||
static uint32_t seq = 0;
|
||||
if (origin_y < 0) origin_y = 0;
|
||||
meow_notify_ipc_notif_open(NULL, ++seq, 0, (uint16_t)origin_y,
|
||||
(uint16_t)meow_ui_statusbar_hotzone(),
|
||||
(uint8_t)(g_fullscreen ? 1 : 0));
|
||||
}
|
||||
|
||||
int meow_ui_sys_statusbar_force(void)
|
||||
{
|
||||
int f = g_statusbar_force;
|
||||
g_statusbar_force = 0;
|
||||
return f;
|
||||
}
|
||||
|
||||
/* ---- 右侧信息簇(MD3 图标 + 变量值,取代文本式 statusbar_right_str)----
|
||||
* 「同一个状态栏」右侧 = 图标+值:电量图标+% / 音量图标+% / 蓝牙图标 / WiFi 图标(信号分档)/
|
||||
* 亮度图标+%(可选,与时钟不重叠才画)。图标位图来自 sb_icons.h(复用 launcher);
|
||||
* 文字=紧凑数值(%d%%),无 CJK 依赖;颜色随 meow_theme(日/夜)由主题表提供。
|
||||
* 顺序(右→左):电量(最右)→ 音量 → 蓝牙 → WiFi → 亮度。 */
|
||||
/* 注:旧文本式右侧串 statusbar_right_str 已退役为图标簇(同上),不再保留调用路径。 */
|
||||
|
||||
/* 画一个右对齐项:右边界=*xr,画完向左推进 *xr(含 MEOW_SB_XGAP 项间隙)。
|
||||
* icon>=0 画图标(可配右侧数值 txt;txt==NULL 仅图标)。文字/图标同用主题色 col。
|
||||
* ink_h 传给 meow_gfx_blit_icon(主图标用 MEOW_SB_ICON_INK 等大;WiFi 强弱档传 0 保持自然 ink)。
|
||||
* ycy = 图标 ink 垂直中心线(收合上移时随 -off 上移);ty = 文字 y。返回该项左边缘(供亮度可选判宽)。 */
|
||||
static int sb_r_place(int *xr, int icon, const char *txt, uint32_t col, int ink_h, int ty, int ycy)
|
||||
{
|
||||
int iconw = (icon >= 0) ? MEOW_SB_ICON_SIZE : 0;
|
||||
int gap = (icon >= 0 && txt) ? MEOW_SB_TGAP : 0;
|
||||
int tw = txt ? meow_ui_text_width(txt, MUI_SBAR_FONT) : 0;
|
||||
int x = *xr - (iconw + gap + tw);
|
||||
if (icon >= 0) { meow_gfx_blit_icon(icon, x, 0, ink_h, col, ycy); x += iconw + gap; }
|
||||
if (txt) meow_ui_text_right(*xr, ty, txt, MUI_SBAR_FONT, col);
|
||||
*xr -= (iconw + gap + tw) + MEOW_SB_XGAP;
|
||||
return *xr + MEOW_SB_XGAP;
|
||||
}
|
||||
|
||||
/* WiFi 信号分档(g_sys.wifi_connected + wifi_level)→ 图标索引。
|
||||
* 网卡关 → -1(不画);开未关联 → SB_I_WIFI_NO;已关联 → 按 dBm 分 STRONG/MEDIUM/WEAK(未知→满格)。 */
|
||||
static int sb_wifi_icon(void)
|
||||
{
|
||||
if (!g_sys.wifi_on) return MEOW_SB_ICON_WIFI_OFF;
|
||||
if (!g_sys.wifi_connected) return MEOW_SB_ICON_WIFI_NO;
|
||||
if (g_sys.wifi_level <= -1000) return MEOW_SB_ICON_WIFI_STRONG; /* 未知 → 满格 */
|
||||
if (g_sys.wifi_level >= -60) return MEOW_SB_ICON_WIFI_STRONG;
|
||||
if (g_sys.wifi_level >= -75) return MEOW_SB_ICON_WIFI_MEDIUM;
|
||||
return MEOW_SB_ICON_WIFI_WEAK;
|
||||
}
|
||||
|
||||
void meow_ui_sys_statusbar_render(void)
|
||||
{
|
||||
meow_ui_sys_statusbar_render_off(0);
|
||||
}
|
||||
|
||||
void meow_ui_sys_statusbar_render_off(int yoff)
|
||||
{
|
||||
if (!g_sys_render) return; /* 仅系统进程(launcher)渲染;App 不画顶条 */
|
||||
if (!meow_ui_sys_statusbar_active()) return;
|
||||
if (yoff < 0) yoff = 0;
|
||||
if (yoff > MUI_SBAR_H) yoff = MUI_SBAR_H;
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int w = meow_ui_width();
|
||||
int h = MUI_SBAR_H;
|
||||
int vis = h - yoff; /* 收合上移:可见条高 = 28-yoff(其余行=App 内容,由 launcher 保留) */
|
||||
if (vis <= 0) return; /* 完全收合由 statusbar_collapsed_render(提示线)处理 */
|
||||
|
||||
/* 整条清底(仅可见条高 [0,vis) 行):覆盖 app 可能画进顶行的内容(z 最顶)。仅系统进程渲染本函数。 */
|
||||
meow_ui_fill_rect(0, 0, w, vis, th->bg);
|
||||
|
||||
/* 文字垂直锚 y:(栏高-行高)/2 居中;下限 1 防极端行高 > 栏高时负 y 溢出顶端(不越界优先)。
|
||||
* 收合上移:内容整体上移 yoff 像素并裁顶(meow_font/图标 blit 已带 y<0 边界钳制,安全)。 */
|
||||
int ty = (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2;
|
||||
if (ty < 1) ty = 1;
|
||||
ty -= yoff;
|
||||
int ycy = MEOW_SB_ICON_CY - yoff; /* 图标 ink 垂直中心随上移 */
|
||||
|
||||
/* 时钟(左):每秒现取 localtime */
|
||||
char timebuf[16];
|
||||
time_t t = time(NULL);
|
||||
struct tm tm;
|
||||
localtime_r(&t, &tm);
|
||||
snprintf(timebuf, sizeof timebuf, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec);
|
||||
meow_ui_text(8, ty, timebuf, MUI_SBAR_FONT, th->fg);
|
||||
|
||||
/* 右侧 MD3 图标簇(从右端 w-8 往左)。主项色 th->fg;次级(蓝牙/弱信号)用 on_surface_variant。
|
||||
* 亮度「可选」:仅在右簇左缘与时钟右缘相距足够(MEOW_SB_CLK_MARGIN)时画,避免与时钟叠字。 */
|
||||
uint32_t col = th->fg, col2 = th->dim; /* col=主色;col2=次要色(蓝牙/分隔用 on_surface_variant=dim 近似) */
|
||||
int xr = w - 8;
|
||||
char val[16];
|
||||
if (g_sys.batt >= 0) { /* 电量:图标+%(未知不画)——最右项 */
|
||||
snprintf(val, sizeof val, "%d%%", g_sys.batt);
|
||||
sb_r_place(&xr, MEOW_SB_ICON_BATT, val, col, MEOW_SB_ICON_INK, ty, ycy);
|
||||
if (g_sys.charging) /* 充电中:电量图标左邻加闪电(M2.92) */
|
||||
sb_r_place(&xr, MEOW_SB_ICON_BATT_CHG, NULL, col, MEOW_SB_ICON_INK, ty, ycy);
|
||||
}
|
||||
snprintf(val, sizeof val, "%d%%", g_sys.vol);
|
||||
sb_r_place(&xr, MEOW_SB_ICON_VOL, val, col, MEOW_SB_ICON_INK, ty, ycy); /* 音量:图标+% */
|
||||
if (g_sys.bt_seen && g_sys.bt_on) { /* 蓝牙:仅图标(上电即显示,连接态占位;多态留后续) */
|
||||
sb_r_place(&xr, MEOW_SB_ICON_BT_CONN, NULL, col2, MEOW_SB_ICON_INK, ty, ycy);
|
||||
}
|
||||
if (g_sys.wifi_on) { /* WiFi:仅图标(关不画;开未连/分档) */
|
||||
int wco = sb_wifi_icon();
|
||||
int is_weak = (wco == MEOW_SB_ICON_WIFI_MEDIUM || wco == MEOW_SB_ICON_WIFI_WEAK);
|
||||
sb_r_place(&xr, wco, NULL, col, is_weak ? 0 : MEOW_SB_ICON_INK, ty, ycy);
|
||||
}
|
||||
if (g_sys.brightness > 0) { /* 亮度:图标+%(>0 才显示;与时钟不重叠才画) */
|
||||
int bl = (g_sys.brightness > 100) ? 100 : g_sys.brightness;
|
||||
snprintf(val, sizeof val, "%d%%", bl);
|
||||
int bw = MEOW_SB_ICON_SIZE + MEOW_SB_TGAP + meow_ui_text_width(val, MUI_SBAR_FONT) + MEOW_SB_XGAP;
|
||||
if (xr - bw > 8 + meow_ui_text_width(timebuf, MUI_SBAR_FONT) + MEOW_SB_CLK_MARGIN)
|
||||
sb_r_place(&xr, MEOW_SB_ICON_BRIGHT, val, col, MEOW_SB_ICON_INK, ty, ycy);
|
||||
}
|
||||
|
||||
/* 分隔线(栏内最底 1px,主题次要色);上移后仍在可见条高内;内容 inset=statusbar_height() 从该线之下即干净 */
|
||||
int dy = h - 1 - yoff;
|
||||
if (dy >= 0 && dy < vis) meow_ui_fill_rect(0, dy, w, 1, th->dim);
|
||||
|
||||
g_statusbar_force = 0;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* meow_ui_sys.h — SDK 内置系统状态读取器 + 全局状态栏(A-0 核心)。
|
||||
*
|
||||
* 供 meow_ui 内部各 TU 协作(绘制后端 present() 前置覆盖 + 输入层事件循环 1s 轻刷)。
|
||||
* 不随 meow_ui.h 发布给应用。应用侧只通过公共 API 控制显隐/全屏:
|
||||
* meow_ui_set_statusbar() / meow_ui_statusbar_visible() /
|
||||
* meow_ui_set_fullscreen() / meow_ui_statusbar_height()。
|
||||
*
|
||||
* 数据源与 launcher 的 meow_sys 同源单点一致:
|
||||
* - 电量 /sys/class/power_supply/battery/capacity(充电态权威源=mp2731-charger/status,
|
||||
* 回退 fusb302-typec-source/online → battery/status|ac/online;battery/status 会撒谎)
|
||||
* - WiFi /sys/class/net/wlan0/operstate
|
||||
* - 蓝牙 实际 powered(hciconfig -a hci0 的 UP;spawn 贵 → 缓存 30s 刷一次)
|
||||
* - 音量 /usr/data/meow/settings.conf 的 audio.volume(launcher 同文件;
|
||||
* 0-100 并 clamp 到 audio.volume_max 上限,与 launcher vol_clamp 口径一致)
|
||||
* - 时钟 localtime_r(每秒在绘制时现取)
|
||||
*/
|
||||
#ifndef MEOW_UI_SYS_H
|
||||
#define MEOW_UI_SYS_H
|
||||
|
||||
#include "meow_ui.h"
|
||||
|
||||
/* 系统快照(状态栏数据源)。时钟不走这里——绘制时现取 localtime。 */
|
||||
typedef struct meow_sys_state {
|
||||
int batt; /* 电量 %;-1=未知 */
|
||||
int charging; /* 1=充电/满 */
|
||||
int wifi_on; /* 网卡 up(射频开;launcher 喂入按 g_sys.wifi_on) */
|
||||
int wifi_connected;/* 已关联 AP(operstate up);launcher 喂入(g_sys.wifi_connected)。WiFi 分档用 */
|
||||
int wifi_level; /* 信号 dBm(-1000=未知);launcher 喂入(-60 强 / -75 中 / 更弱弱)。 */
|
||||
int bt_seen; /* hci0 节点存在 */
|
||||
int bt_on; /* 实际上电(hciconfig UP) */
|
||||
int vol; /* 音量 0-100 */
|
||||
int brightness; /* 背光/亮度 1-100;只有 >0 才显示(屏常亮态)。由 launcher 喂入
|
||||
* (launcher 是背光写入/clamp 方),SDK 自己亦用 settings 兜底读。 */
|
||||
} meow_sys_state;
|
||||
|
||||
/* 顶部状态栏几何(与 launcher SB_BAR_H=28 / SB_FONT=20 对齐,视觉统一)。
|
||||
* 高度 40→28(用户定),字号 28→20(28px 栏放不下 28 号;20 号按 (高-行高)/2 垂直居中)。 */
|
||||
#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);
|
||||
|
||||
/* 1s 轻刷:电量/WiFi/音量(便宜);蓝牙 30s 缓存刷新。 */
|
||||
void meow_ui_sys_refresh(void);
|
||||
|
||||
/* 当前快照(内部只读)。 */
|
||||
const meow_sys_state *meow_ui_sys_get(void);
|
||||
|
||||
/* 状态栏是否应绘制(= set_statusbar(1) 且 非 fullscreen),收合(collapsed)兼容:
|
||||
* collapsed 视为状态栏仍在(只留提示线),故 collapsed 时即使 fullscreen 也返回 1,
|
||||
* 让渲染端/输入层继续驱动上收动画与提示线;fullscreen 且不 collapsed → 0(整条隐藏,
|
||||
* 沉浸,保持既有语义)。 */
|
||||
int meow_ui_sys_statusbar_active(void);
|
||||
|
||||
/* 状态栏有效高度(px,供 inset/提示条定位):active 时 28;collapsed 时=提示线高度
|
||||
* MUI_SBAR_COLLAPSE_H(非满 28,app 内容可上移);否则 0(供 meow_ui_statusbar_height 用)。 */
|
||||
int meow_ui_sys_statusbar_height(void);
|
||||
|
||||
/* 系统级「下拉展开通知中心」手势挂钩(§10.5):输入层识别出「起始于顶部热区的 SWIPE_DOWN」
|
||||
* 后路由到这里(origin_y=手势按下原点 Y,供 launcher 复核是否在顶部热区)。App 进程只做
|
||||
* 检测+上报:经 IPC 发 MSG_NOTIF_OPEN 请求 launcher 展开通知中心叠加会话;**App 不渲染系统 UI**。
|
||||
* best-effort:无 socket/launcher 不在 → 静默降级。 */
|
||||
void meow_ui_sys_notif_pull(int origin_y);
|
||||
|
||||
/* 自上次渲染后状态栏数据是否已变(1s 轻刷/显隐切换置位);读取即清除。
|
||||
* 渲染后端 present() 用它决定是否重画顶部条(配合脏区/整帧判断)。 */
|
||||
int meow_ui_sys_statusbar_force(void);
|
||||
|
||||
/* 把状态栏画进 back_buf(顶部 MUI_SBAR_H 行)+ 标脏。调用方负责随后上屏。 */
|
||||
void meow_ui_sys_statusbar_render(void);
|
||||
|
||||
/* 收合「上移出屏」过渡渲染(仅系统进程/launcher,见 meow_ui_sys_statusbar_render):内容整体上移
|
||||
* yoff 像素并裁顶;只画可见条高 [0, MUI_SBAR_H-yoff) 行,其余行保留(App 内容由 launcher 标脏切齐)。
|
||||
* yoff=0 等价 meow_ui_sys_statusbar_render()。实现见 meow_ui_sys.c。 */
|
||||
void meow_ui_sys_statusbar_render_off(int yoff);
|
||||
|
||||
/* 把当前顶条模式写到 /tmp/meowapp_sbar(仅 App 进程 !g_sys_render 才写;launcher 读)。
|
||||
* meow_ui_statusbar_set_collapsed(gfx.c)也调用它,故在头文件声明。 */
|
||||
void meow_ui_sbar_write_mode(void);
|
||||
|
||||
#endif /* MEOW_UI_SYS_H */
|
||||
@@ -0,0 +1,382 @@
|
||||
/*
|
||||
* meow_ui_theme.c — 主题/配置(B1.5 / M2.46)。
|
||||
*
|
||||
* 内置日间/夜间两套色板;模式跟随系统设置键 display.theme_mode + display.theme
|
||||
* (/usr/data/meow/settings.conf,与 launcher meow_settings 同源)。
|
||||
* 判定与 launcher 的 s_ui_theme() 完全对齐(避免两套逻辑漂移):
|
||||
* - display.theme_mode = manual:直读 display.theme(dark/light)决定深浅。
|
||||
* - display.theme_mode = auto:按深色时段 display.theme_dark_start/end(分钟 0-1439),
|
||||
* 半开区间 [start,end);start>end 跨午夜;start==end 视为未配置 → 回退 manual(display.theme)。
|
||||
* 读取缺失/非法一律回退深色(与 launcher 默认一致)。
|
||||
* 切换即广播 listener。AUTO 解析只在查询时按当前系统时间计算(不缓存)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_theme_reload() 定义(供 launcher 逼重读 settings) */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||||
|
||||
/* ---- 主题表(§5.8.4 + §8.1.3):[style][dark] ———
|
||||
* 0=MEOW_STYLE_MD3, 1=MEOW_STYLE_MIUIX;dark 0=light, 1=dark。
|
||||
* 旧字段 bg/fg/accent/dim = surface/onSurface/primary/onSurfaceVariant(映射见 meow_ui.h)。
|
||||
* bg/surface 随后由「背景全局源」(display.background,§8.1.4)按 style×mode 覆写。 */
|
||||
static meow_theme g_theme[2][2];
|
||||
|
||||
static meow_theme_mode g_mode = MEOW_THEME_DARK; /* 配置生效的判定模式(默认深色,与 launcher 默认一致) */
|
||||
static meow_theme_mode g_manual = MEOW_THEME_DARK; /* display.theme 解析出的手动深浅;auto 未配置时段时回退用之 */
|
||||
static int g_dark_start = 0; /* 深色时段开始(分钟 0-1439) */
|
||||
static int g_dark_end = 0; /* 深色时段结束(分钟 0-1439) */
|
||||
static int g_loaded = 0;
|
||||
static int g_style = MEOW_STYLE_MD3; /* 顶级主题风格族(§8.1.2):默认 MD3 */
|
||||
static char g_background[64] = ""; /* display.background(black/blue/green/white/… 或图片路径) */
|
||||
|
||||
static meow_theme_listener g_listeners[8];
|
||||
static int g_nlisteners = 0;
|
||||
|
||||
/* ---- 读 /usr/data/meow/settings.conf 的 key=value(兼容 "key = value" 带空格形式)----
|
||||
* 与 launcher meow_settings_import 同款解析:按 '=' 切分,key/value 各自去首尾空白,
|
||||
* 精确匹配 key(避免 display.theme 误匹配 display.theme_mode)。返回 0=命中,-1=未命中。 */
|
||||
static int get_setting_str(const char *key, char *buf, int buflen)
|
||||
{
|
||||
FILE *f = fopen(MEOW_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;
|
||||
/* key = 行首到 '=',去尾空白 */
|
||||
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;
|
||||
/* value = '=' 后,去首尾空白 / 行尾 CRLF */
|
||||
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;
|
||||
}
|
||||
|
||||
static int get_setting_int(const char *key, int *out)
|
||||
{
|
||||
char buf[32];
|
||||
if (get_setting_str(key, buf, sizeof buf) != 0) return -1;
|
||||
*out = atoi(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 解析 display.theme 的值(dark/light,非索引)→ LIGHT/DARK;未知/缺失回退 DARK(launcher 默认)。 */
|
||||
static meow_theme_mode parse_manual(const char *v)
|
||||
{
|
||||
while (*v == ' ' || *v == '\t') v++;
|
||||
if (!strncmp(v, "light", 5)) return MEOW_THEME_LIGHT;
|
||||
return MEOW_THEME_DARK; /* "dark" / 未知 → 深色 */
|
||||
}
|
||||
|
||||
/* ---- 填充 4 张主题表的内建色板(§8.1.3:MD3 / MIUIx 各一整套,light/dark 各一)----
|
||||
* bg/surface 本函数填默认面;随后 apply_background() 按 display.background 按 style 覆写。 */
|
||||
static void init_defaults(void)
|
||||
{
|
||||
meow_theme *t;
|
||||
|
||||
/* MD3 light */
|
||||
t = &g_theme[MEOW_STYLE_MD3][0];
|
||||
t->bg = 0xE8EAF0; t->fg = 0x1A1A2A; t->accent = 0x2A5A8A; t->dim = 0x4A4F5A;
|
||||
t->font_title = 28; t->font_body = 20;
|
||||
t->primary = 0x2A5A8A; t->onPrimary = 0xFFFFFF; t->primaryContainer = 0xC9E2F5; t->onPrimaryContainer = 0x0A2B46;
|
||||
t->secondary = 0x53626E; t->onSecondary = 0xFFFFFF; t->secondaryContainer = 0xD6E5F2; t->onSecondaryContainer = 0x101E28;
|
||||
t->tertiary = 0x635B78; t->onTertiary = 0xFFFFFF; t->tertiaryContainer = 0xE9DEFB; t->onTertiaryContainer = 0x1E1734;
|
||||
t->surface = 0xE8EAF0; t->onSurface = 0x1A1A2A; t->surfaceVariant = 0xDDE1EA; t->onSurfaceVariant = 0x4A4F5A;
|
||||
t->surfaceContainerLowest = 0xFFFFFF; t->surfaceContainerLow = 0xF2F3F7; t->surfaceContainer = 0xECEEF3;
|
||||
t->surfaceContainerHigh = 0xE6E8EE; t->surfaceContainerHighest = 0xE1E3E9;
|
||||
t->surfaceDim = 0xD8DAE0; t->surfaceBright = 0xFFFFFF; t->surfaceTint = 0x2A5A8A;
|
||||
t->outline = 0x74777F; t->outlineVariant = 0xC4C6D0; t->scrim = 0x000000;
|
||||
t->error = 0xBA1A1A; t->onError = 0xFFFFFF; t->errorContainer = 0xFFDAD6; t->onErrorContainer = 0x410002;
|
||||
t->inverseSurface = 0x2F3036; t->inverseOnSurface = 0xF0F0F5; t->inversePrimary = 0xA6C8EE;
|
||||
t->shadow = 0x000000;
|
||||
t->radius = 12; t->radius_card = 16; t->spacing = 8; t->stroke_w = 0;
|
||||
|
||||
/* MD3 dark */
|
||||
t = &g_theme[MEOW_STYLE_MD3][1];
|
||||
t->bg = 0x141824; t->fg = 0xFFFFFF; t->accent = 0x4A6A9A; t->dim = 0x9AA4C8;
|
||||
t->font_title = 28; t->font_body = 20;
|
||||
t->primary = 0x4A6A9A; t->onPrimary = 0xFFFFFF; t->primaryContainer = 0x2A3A5C; t->onPrimaryContainer = 0xD5E4F5;
|
||||
t->secondary = 0xB8C6D2; t->onSecondary = 0x27323B; t->secondaryContainer = 0x3A4650; t->onSecondaryContainer = 0xD6E5F2;
|
||||
t->tertiary = 0xCDC2E6; t->onTertiary = 0x372C52; t->tertiaryContainer = 0x4E4269; t->onTertiaryContainer = 0xE9DEFB;
|
||||
t->surface = 0x141824; t->onSurface = 0xFFFFFF; t->surfaceVariant = 0x2A2F3C; t->onSurfaceVariant = 0x9AA4C8;
|
||||
t->surfaceContainerLowest = 0x0F121C; t->surfaceContainerLow = 0x1C2030; t->surfaceContainer = 0x20253A;
|
||||
t->surfaceContainerHigh = 0x262C44; t->surfaceContainerHighest = 0x2C334E;
|
||||
t->surfaceDim = 0x12151E; t->surfaceBright = 0x3A3E50; t->surfaceTint = 0x4A6A9A;
|
||||
t->outline = 0x8B93A3; t->outlineVariant = 0x3E4450; t->scrim = 0x000000;
|
||||
t->error = 0xFFB4AB; t->onError = 0x690005; t->errorContainer = 0x93000A; t->onErrorContainer = 0xFFDAD6;
|
||||
t->inverseSurface = 0xF0F0F5; t->inverseOnSurface = 0x2F3036; t->inversePrimary = 0xA6C8EE;
|
||||
t->shadow = 0x000000;
|
||||
t->radius = 12; t->radius_card = 16; t->spacing = 8; t->stroke_w = 0;
|
||||
|
||||
/* MIUIx(§8.1.3 定案):完整独立色板(不复刻 MD3)——暖色基面 + 小米橙 primary +
|
||||
* 更大圆角/更松间距/细描边。观感:MIUI 系暖白(浅)/暖黑(深)、橙强调、蓝作 tertiary 辅助。
|
||||
* 与 MD3 保持可感知差异(圆角/描边/间距/主色),避免"切了没感觉"。 */
|
||||
t = &g_theme[MEOW_STYLE_MIUIX][0]; /* MIUIx light */
|
||||
t->bg = 0xF7F3EE; t->fg = 0x251A10; t->accent = 0xF57200; t->dim = 0x7D6A5A;
|
||||
t->font_title = 28; t->font_body = 20;
|
||||
t->primary = 0xF57200; t->onPrimary = 0xFFFFFF; t->primaryContainer = 0xFFE2CC; t->onPrimaryContainer = 0x5E2B00;
|
||||
t->secondary = 0x8A6F5C; t->onSecondary = 0xFFFFFF; t->secondaryContainer = 0xF0E2D6; t->onSecondaryContainer = 0x3A2A1C;
|
||||
t->tertiary = 0x3A5FA8; t->onTertiary = 0xFFFFFF; t->tertiaryContainer = 0xD6E2FA; t->onTertiaryContainer = 0x1E2C4A;
|
||||
t->surface = 0xF7F3EE; t->onSurface = 0x251A10; t->surfaceVariant = 0xEDE2D7; t->onSurfaceVariant = 0x7D6A5A;
|
||||
t->surfaceContainerLowest = 0xFFFFFF; t->surfaceContainerLow = 0xFAF6F1; t->surfaceContainer = 0xF4EDE3;
|
||||
t->surfaceContainerHigh = 0xEEE6D9; t->surfaceContainerHighest = 0xE9DFD0;
|
||||
t->surfaceDim = 0xDFD3C4; t->surfaceBright = 0xFFFBF6; t->surfaceTint = 0xF57200;
|
||||
t->outline = 0x85746A; t->outlineVariant = 0xCBBFB2; t->scrim = 0x000000;
|
||||
t->error = 0xB3261E; t->onError = 0xFFFFFF; t->errorContainer = 0xFFDAD6; t->onErrorContainer = 0x410002;
|
||||
t->inverseSurface = 0x322A22; t->inverseOnSurface = 0xF6EFE6; t->inversePrimary = 0xFFB58C;
|
||||
t->shadow = 0x000000;
|
||||
t->radius = 20; t->radius_card = 28; t->spacing = 12; t->stroke_w = 1;
|
||||
|
||||
t = &g_theme[MEOW_STYLE_MIUIX][1]; /* MIUIx dark */
|
||||
t->bg = 0x181310; t->fg = 0xEDE5DC; t->accent = 0xFF8C4A; t->dim = 0xAB9884;
|
||||
t->font_title = 28; t->font_body = 20;
|
||||
t->primary = 0xFF8C4A; t->onPrimary = 0x241100; t->primaryContainer = 0x5E2B00; t->onPrimaryContainer = 0xFFDDC8;
|
||||
t->secondary = 0xC9B5A2; t->onSecondary = 0x34261A; t->secondaryContainer = 0x4A3A2C; t->onSecondaryContainer = 0xEADCCD;
|
||||
t->tertiary = 0x9EB8EC; t->onTertiary = 0x1E2C4A; t->tertiaryContainer = 0x2E4370; t->onTertiaryContainer = 0xD6E2FA;
|
||||
t->surface = 0x181310; t->onSurface = 0xEDE5DC; t->surfaceVariant = 0x332B25; t->onSurfaceVariant = 0xAB9884;
|
||||
t->surfaceContainerLowest = 0x120E0B; t->surfaceContainerLow = 0x211B16; t->surfaceContainer = 0x262019;
|
||||
t->surfaceContainerHigh = 0x302922; t->surfaceContainerHighest = 0x3A3229;
|
||||
t->surfaceDim = 0x120E0B; t->surfaceBright = 0x3F372E; t->surfaceTint = 0xFF8C4A;
|
||||
t->outline = 0x8A7B6B; t->outlineVariant = 0x4A3F35; t->scrim = 0x000000;
|
||||
t->error = 0xFFB4AB; t->onError = 0x690005; t->errorContainer = 0x93000A; t->onErrorContainer = 0xFFDAD6;
|
||||
t->inverseSurface = 0xEDE5DC; t->inverseOnSurface = 0x322A22; t->inversePrimary = 0xF57200;
|
||||
t->shadow = 0x000000;
|
||||
t->radius = 20; t->radius_card = 28; t->spacing = 12; t->stroke_w = 1;
|
||||
}
|
||||
|
||||
/* ---- 背景全局源(§8.1.4):display.background 是「style×mode 下背景底选项集」的当前值,
|
||||
* 决定主题 surface/bg(深浅成对),且随 style 换族(§8.1.1 矩阵 MD3/MIUIx 各一套)。
|
||||
* 按名映射 (style, dark_surface, light_surface):
|
||||
* MD3:dark 默认/black → 0x141824 / light 默认/white → 0xE8EAF0(既有默认,App 无回归);
|
||||
* blue → 0x0A1428 / 0xE5EEFB;green → 0x0A2814 / 0xE6F6E8。
|
||||
* MIUIx:dark 默认/black → 0x181310 / light 默认/white → 0xF7F3EE(暖色族);
|
||||
* blue → 0x101A2E / 0xEDEAF5;green → 0x14251A / 0xEFF3EC。
|
||||
* 图片路径(*.png/jpg/jpeg):背景图即观感 → 用该 style 的默认 surface(本批草案)。 */
|
||||
static int bg_is_image(const char *v)
|
||||
{
|
||||
while (*v == ' ' || *v == '\t') v++;
|
||||
const char *dot = strrchr(v, '.');
|
||||
if (!dot || dot == v) return 0;
|
||||
return !strncasecmp(dot, ".png", 4) || !strncasecmp(dot, ".jpg", 4) || !strncasecmp(dot, ".jpeg", 5);
|
||||
}
|
||||
static void bg_surfaces(int style, const char *v, uint32_t *dark, uint32_t *light)
|
||||
{
|
||||
/* 默认面(未知/图片/空)→ 各 style 的默认 surface(MIUIx 暖色族) */
|
||||
if (style == MEOW_STYLE_MIUIX) { *dark = 0x181310; *light = 0xF7F3EE; }
|
||||
else { *dark = 0x141824; *light = 0xE8EAF0; }
|
||||
while (*v == ' ' || *v == '\t') v++;
|
||||
if (bg_is_image(v)) return; /* 背景图:用该 style 默认面 */
|
||||
if (style == MEOW_STYLE_MIUIX) {
|
||||
if (!strncmp(v, "blue", 4)) { *dark = 0x101A2E; *light = 0xEDEAF5; return; }
|
||||
if (!strncmp(v, "green", 5)) { *dark = 0x14251A; *light = 0xEFF3EC; return; }
|
||||
if (!strncmp(v, "white", 5)) { *dark = 0x181310; *light = 0xFFFFFF; return; }
|
||||
if (!strncmp(v, "lightblue", 9)) { *dark = 0x101A2E; *light = 0xEDEAF5; return; }
|
||||
if (!strncmp(v, "lightgreen", 10)) { *dark = 0x14251A; *light = 0xEFF3EC; return; }
|
||||
return; /* black / 其它未知 → MIUIx 默认暖面 */
|
||||
}
|
||||
/* MD3(既有语义,保持不变) */
|
||||
if (!strncmp(v, "blue", 4)) { *dark = 0x0A1428; *light = 0xE5EEFB; return; }
|
||||
if (!strncmp(v, "green", 5)) { *dark = 0x0A2814; *light = 0xE6F6E8; return; }
|
||||
if (!strncmp(v, "white", 5)) { *dark = 0x141824; *light = 0xFFFFFF; return; }
|
||||
if (!strncmp(v, "lightblue", 9)) { *dark = 0x0A1428; *light = 0xE5EEFB; return; }
|
||||
if (!strncmp(v, "lightgreen", 10)) { *dark = 0x0A2814; *light = 0xE6F6E8; return; }
|
||||
/* black / 其它未知 → 默认面 */
|
||||
}
|
||||
static void apply_background(void)
|
||||
{
|
||||
uint32_t ds, ls;
|
||||
for (int s = 0; s < 2; s++) {
|
||||
/* 图片背景:底色即观感 → 保持 init_defaults 各 style 的默认 surface(不覆写) */
|
||||
if (bg_is_image(g_background)) continue;
|
||||
bg_surfaces(s, g_background, &ds, &ls);
|
||||
g_theme[s][0].bg = g_theme[s][0].surface = ls; /* light 行 */
|
||||
g_theme[s][1].bg = g_theme[s][1].surface = ds; /* dark 行 */
|
||||
}
|
||||
}
|
||||
|
||||
static void load_settings(void)
|
||||
{
|
||||
char buf[64];
|
||||
|
||||
g_manual = MEOW_THEME_DARK;
|
||||
g_mode = MEOW_THEME_DARK;
|
||||
g_dark_start = 0;
|
||||
g_dark_end = 0;
|
||||
|
||||
/* display.theme:手动深浅(auto 未配置时段时也回退这里) */
|
||||
if (get_setting_str("display.theme", buf, sizeof buf) == 0)
|
||||
g_manual = parse_manual(buf);
|
||||
|
||||
/* display.theme_mode:manual=读 display.theme;auto=时段。缺失/非法 → manual */
|
||||
if (get_setting_str("display.theme_mode", buf, sizeof buf) == 0) {
|
||||
if (!strcmp(buf, "auto")) g_mode = MEOW_THEME_AUTO;
|
||||
else g_mode = g_manual; /* manual(含 "light"/"dark"/未知)→ 读 display.theme */
|
||||
} else {
|
||||
g_mode = g_manual; /* 无 theme_mode 键 → 默认 manual → display.theme */
|
||||
}
|
||||
|
||||
get_setting_int("display.theme_dark_start", &g_dark_start);
|
||||
get_setting_int("display.theme_dark_end", &g_dark_end);
|
||||
|
||||
/* 顶级主题风格族(§8.1.2 minimal):ui.style 的 md3/miuix。读取缺失/未知 → 回退 MD3。
|
||||
* 决定状态栏右侧图标集(§3.1 同一个状态栏);配色日/夜仍由 display.theme 现有解析管。 */
|
||||
g_style = MEOW_STYLE_MD3;
|
||||
if (get_setting_str("ui.style", buf, sizeof buf) == 0) {
|
||||
const char *v = buf;
|
||||
while (*v == ' ' || *v == '\t') v++;
|
||||
if (!strncmp(v, "miuix", 5) || !strncmp(v, "miui", 4)) g_style = MEOW_STYLE_MIUIX;
|
||||
}
|
||||
|
||||
/* 背景全局源(§8.1.4):display.background 为 style×mode 下背景底选项集的当前值。
|
||||
* 读取缺失/未知 → ""(默认面)。 */
|
||||
g_background[0] = 0;
|
||||
if (get_setting_str("display.background", g_background, sizeof g_background) != 0)
|
||||
g_background[0] = 0;
|
||||
|
||||
/* 载入/覆写 4 张主题表(色板 + style×mode 背景 surface/bg) */
|
||||
init_defaults();
|
||||
apply_background();
|
||||
}
|
||||
|
||||
/* 当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用(与 launcher s_now_min_of_day 一致)。 */
|
||||
static int now_min_of_day(void)
|
||||
{
|
||||
time_t t = time(NULL);
|
||||
struct tm tm;
|
||||
localtime_r(&t, &tm);
|
||||
return tm.tm_hour * 60 + tm.tm_min;
|
||||
}
|
||||
|
||||
/* 深色时段判定(与 launcher theme_should_dark 完全一致):now_min ∈ [start,end) → 深色。
|
||||
* - start==end 视为未配置,返回 -1(由调用方决定回退策略)。
|
||||
* - start<end:非跨天(如 720→1080 = 12:00→18:00)。
|
||||
* - start>end:跨午夜(如 1140→420 = 19:00→次日07:00)。
|
||||
* - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */
|
||||
static int theme_should_dark(int now_min, int start, int end)
|
||||
{
|
||||
if (start == end) return -1; /* 未配置 */
|
||||
if (start < end)
|
||||
return (now_min >= start && now_min < end) ? 1 : 0;
|
||||
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
|
||||
}
|
||||
|
||||
static meow_theme_mode resolve_mode(meow_theme_mode m)
|
||||
{
|
||||
if (m != MEOW_THEME_AUTO) return m;
|
||||
int d = theme_should_dark(now_min_of_day(), g_dark_start, g_dark_end);
|
||||
if (d < 0) return g_manual; /* 时段未配置:回退 display.theme 手动深浅 */
|
||||
return d ? MEOW_THEME_DARK : MEOW_THEME_LIGHT;
|
||||
}
|
||||
|
||||
static void broadcast(void)
|
||||
{
|
||||
for (int i = 0; i < g_nlisteners; i++)
|
||||
if (g_listeners[i]) g_listeners[i](g_mode);
|
||||
}
|
||||
|
||||
/* 当前「style×mode」主题:style=0/1(MD3/MIUIX),dark=0(light)/1(dark)。 */
|
||||
static const meow_theme *theme_current(int style, int dark)
|
||||
{
|
||||
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];
|
||||
}
|
||||
|
||||
const meow_theme *meow_theme_get(void)
|
||||
{
|
||||
if (!g_loaded) {
|
||||
load_settings();
|
||||
g_loaded = 1;
|
||||
}
|
||||
return theme_current(g_style, resolve_mode(g_mode) == MEOW_THEME_DARK);
|
||||
}
|
||||
|
||||
meow_theme_mode meow_theme_get_mode(void)
|
||||
{
|
||||
if (!g_loaded) { load_settings(); g_loaded = 1; }
|
||||
return g_mode;
|
||||
}
|
||||
|
||||
void meow_theme_set_mode(meow_theme_mode m)
|
||||
{
|
||||
if (!g_loaded) { load_settings(); g_loaded = 1; }
|
||||
if (m < MEOW_THEME_LIGHT || m > MEOW_THEME_AUTO) return;
|
||||
if (m == g_mode) return;
|
||||
g_mode = m;
|
||||
broadcast();
|
||||
}
|
||||
|
||||
/* 重置缓存:下一次 meow_theme_get()/get_mode()/defaults() 重读 settings.conf。
|
||||
* launcher 在 statusbar 刷新前调用,让 SDK 全局状态栏(其主题缓存于 g_loaded)跟手
|
||||
* launcher 侧主题变更(settings.conf 被改后立即反映),避免「界面已换肤、状态栏滞留旧主题」。 */
|
||||
void meow_theme_reload(void)
|
||||
{
|
||||
g_loaded = 0;
|
||||
}
|
||||
|
||||
meow_theme meow_theme_defaults(meow_theme_mode m)
|
||||
{
|
||||
if (!g_loaded) { load_settings(); g_loaded = 1; }
|
||||
/* 脱机取内置板:返回 MD3(默认)style 下解析出的模式表。 */
|
||||
return g_theme[MEOW_STYLE_MD3][resolve_mode(m) == MEOW_THEME_DARK ? 1 : 0];
|
||||
}
|
||||
|
||||
/* ---- §8.1.2 顶级主题风格族(minimal)----
|
||||
* meow_ui_style() 返回当前 ui.style(读取走 load_settings,随 meow_theme_reload() 重读,
|
||||
* 故 launcher 改 settings.conf 后经 sb_sdk_refresh→theme_reload 即跟手)。
|
||||
* meow_ui_set_style() 记录 style(越界 clamp;同值忽略)并触发 reload/broadcast——供需要
|
||||
* 显式切换的调用方(后续 launcher 设置页切换可改走此路径);状态栏图标集据此换集。 */
|
||||
int meow_ui_style(void)
|
||||
{
|
||||
if (!g_loaded) { load_settings(); g_loaded = 1; }
|
||||
return g_style;
|
||||
}
|
||||
|
||||
void meow_ui_set_style(int style)
|
||||
{
|
||||
if (style < MEOW_STYLE_MD3) style = MEOW_STYLE_MD3;
|
||||
if (style > MEOW_STYLE_MIUIX) style = MEOW_STYLE_MIUIX;
|
||||
if (style == g_style) return;
|
||||
g_style = style;
|
||||
/* 触发下一帧按新 style 重绘(§8.1.2):g_theme[s][*] 是独立表(不随 g_style 重算),
|
||||
* g_style 记录后 meow_theme_get() 立即可取新 style×mode 表;只需广播主题 listener,
|
||||
* 让 App/状态栏/控件下一帧用新 style 重绘/换图标集。不清 settings 缓存(避免覆盖程序化 style)。 */
|
||||
broadcast();
|
||||
}
|
||||
|
||||
void meow_theme_add_listener(meow_theme_listener cb)
|
||||
{
|
||||
if (!cb) return;
|
||||
if (g_nlisteners >= (int)(sizeof g_listeners / sizeof g_listeners[0])) return;
|
||||
for (int i = 0; i < g_nlisteners; i++) if (g_listeners[i] == cb) return;
|
||||
g_listeners[g_nlisteners++] = cb;
|
||||
}
|
||||
|
||||
void meow_theme_remove_listener(meow_theme_listener cb)
|
||||
{
|
||||
for (int i = 0; i < g_nlisteners; i++) {
|
||||
if (g_listeners[i] == cb) {
|
||||
g_listeners[i] = g_listeners[g_nlisteners - 1];
|
||||
g_nlisteners--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* meow_ui_view.c — meow_view 视图/页面框架骨架(§A 视图数据结构/嵌套/生命周期/页面导航)。
|
||||
*
|
||||
* meow_view 是**薄壳**:容器 + 生命周期 + 事件路由;叶子仍是 meow_widget(不改)。绘制走
|
||||
* z 序绘制物注册表(on_draw 回调,非保留——不缓存像素)。opt-in:不调 meow_view_* 的 App
|
||||
* 完全照旧(widget/activity/flush 不动)。页面模型 = 定案 b(顶层页切走 activity 栈,
|
||||
* page_push/pop/replace 包装;不做页内子导航栈,sub_push/pop 为预留扩展位 a 桩)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_view.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MEOW_VIEW_BY_ID_MAX 32
|
||||
|
||||
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(生命周期跟踪) */
|
||||
|
||||
static void run_fn(meow_view_fn f, meow_view *v)
|
||||
{
|
||||
if (f) f(v, v->ctx);
|
||||
}
|
||||
|
||||
/* 登记/清除 id→view 映射(幂等)。 */
|
||||
static void view_map_set(meow_view *v)
|
||||
{
|
||||
if (!v) return;
|
||||
int id = v->id;
|
||||
if (id >= 0 && id < MEOW_VIEW_BY_ID_MAX) g_view_by_id[id] = v;
|
||||
}
|
||||
static void view_map_clear(meow_view *v)
|
||||
{
|
||||
if (!v) return;
|
||||
int id = v->id;
|
||||
if (id >= 0 && id < MEOW_VIEW_BY_ID_MAX && g_view_by_id[id] == v) g_view_by_id[id] = NULL;
|
||||
}
|
||||
static meow_view *view_map_get(int id)
|
||||
{
|
||||
return (id >= 0 && id < MEOW_VIEW_BY_ID_MAX) ? g_view_by_id[id] : NULL;
|
||||
}
|
||||
|
||||
/* 进入一个 view(置 ENTERED + on_enter),**递归 children**(子视图随父进出,§A.4)。
|
||||
* 进入顺序:父 on_enter 先(记录顺序 home.enter→demo.enter),随后子视图依递归逐个进入;
|
||||
* 子视图的 on_draw 经 meow_draw_view_entered(含祖先已进入判定)保证父 on_enter 重绘时可见。 */
|
||||
static void view_enter(meow_view *v)
|
||||
{
|
||||
if (!v || (v->state & MEOW_VIEW_STATE_ENTERED)) return;
|
||||
v->state |= MEOW_VIEW_STATE_ENTERED;
|
||||
run_fn(v->hooks.on_enter, v);
|
||||
for (int i = 0; i < v->nchildren; i++)
|
||||
view_enter(v->children[i]);
|
||||
}
|
||||
/* 离开一个 view(清 ENTERED + on_leave;默认不销毁子 view/widget——弹回零重建),
|
||||
* **递归 children**:先子后父(demo.leave→home.leave)。 */
|
||||
static void view_leave(meow_view *v)
|
||||
{
|
||||
if (!v || !(v->state & MEOW_VIEW_STATE_ENTERED)) return;
|
||||
for (int i = 0; i < v->nchildren; i++)
|
||||
view_leave(v->children[i]);
|
||||
v->state &= ~MEOW_VIEW_STATE_ENTERED;
|
||||
run_fn(v->hooks.on_leave, v);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 生命周期 */
|
||||
/* ============================================================ */
|
||||
meow_view *meow_view_create(int id, meow_view_type type,
|
||||
const meow_view_hooks *hooks, void *ctx)
|
||||
{
|
||||
meow_view *v = (meow_view *)calloc(1, sizeof *v);
|
||||
if (!v) return NULL;
|
||||
v->id = id;
|
||||
v->type = (type == MEOW_VIEW_CONTAINER) ? MEOW_VIEW_CONTAINER : MEOW_VIEW_ACTIVITY;
|
||||
v->x = v->y = v->w = v->h = 0;
|
||||
v->z = 0;
|
||||
v->state = MEOW_VIEW_STATE_CREATED;
|
||||
if (hooks) v->hooks = *hooks;
|
||||
v->ctx = ctx;
|
||||
view_map_set(v);
|
||||
meow_draw_view_register(v); /* 登记为绘制物(on_draw 经 z 序重画触发) */
|
||||
run_fn(v->hooks.on_create, v);
|
||||
if (v->hooks.on_layout) run_fn(v->hooks.on_layout, v); /* 布局:create 后跑一次 */
|
||||
return v;
|
||||
}
|
||||
|
||||
void meow_view_free(meow_view *v)
|
||||
{
|
||||
if (!v) return;
|
||||
if (v->state & MEOW_VIEW_STATE_ENTERED) view_leave(v);
|
||||
run_fn(v->hooks.on_destroy, v); /* 释放视图态资源 */
|
||||
view_map_clear(v);
|
||||
meow_draw_view_unregister(v); /* 注销 z 序绘制物 */
|
||||
for (int i = 0; i < v->nwidgets; i++) /* 叶子控件 */
|
||||
meow_widget_free(v->widgets[i]);
|
||||
v->nwidgets = 0;
|
||||
for (int i = 0; i < v->nchildren; i++) /* 子 view 递归 */
|
||||
meow_view_free(v->children[i]);
|
||||
v->nchildren = 0;
|
||||
free(v);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 嵌套(组合视图树) */
|
||||
/* ============================================================ */
|
||||
int meow_view_add_child(meow_view *parent, meow_view *child)
|
||||
{
|
||||
if (!parent || !child || child == parent) return -1;
|
||||
for (int i = 0; i < parent->nchildren; i++)
|
||||
if (parent->children[i] == child) return -1; /* 去重 */
|
||||
if (parent->nchildren >= MEOW_VIEW_MAX_CHILD) return -1;
|
||||
parent->children[parent->nchildren++] = child;
|
||||
child->parent = parent;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_view_add_widget(meow_view *v, meow_widget *w)
|
||||
{
|
||||
if (!v || !w) return -1;
|
||||
for (int i = 0; i < v->nwidgets; i++)
|
||||
if (v->widgets[i] == w) return -1;
|
||||
if (v->nwidgets >= MEOW_VIEW_MAX_WIDGET) return -1;
|
||||
v->widgets[v->nwidgets++] = w;
|
||||
meow_draw_widget_set_view(w, v); /* z 深度源 = view 树;重算 auto z */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void meow_view_set_z(meow_view *v, int z)
|
||||
{
|
||||
if (!v) return;
|
||||
v->z = (z < 0) ? 0 : z;
|
||||
meow_draw_view_set_z(v, v->z);
|
||||
}
|
||||
int meow_view_z(const meow_view *v)
|
||||
{
|
||||
return v ? v->z : 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页面导航(包装 activity 栈——唯一驱动 on_enter/on_leave) */
|
||||
/* ============================================================ */
|
||||
void meow_view_page_push(int id)
|
||||
{
|
||||
meow_view *cur = g_top_view;
|
||||
meow_view *next = view_map_get(id);
|
||||
if (cur && cur != next) view_leave(cur);
|
||||
meow_ui_activity_push(id);
|
||||
view_enter(next);
|
||||
g_top_view = next ? next : cur;
|
||||
}
|
||||
|
||||
void meow_view_page_pop(void)
|
||||
{
|
||||
meow_view *cur = g_top_view;
|
||||
view_leave(cur);
|
||||
meow_ui_activity_pop();
|
||||
meow_view *next = view_map_get(meow_ui_activity_active());
|
||||
view_enter(next);
|
||||
g_top_view = next ? next : NULL;
|
||||
}
|
||||
|
||||
/* 说明(诚实):replace 假定目标页已由 App 先 meow_view_create 指定 id 建好;本批不销毁旧页
|
||||
* 对象(保留弹回),仅 leave+enter 切换——与设计「leave+destroy 当前」的差异见汇报。 */
|
||||
void meow_view_page_replace(int id)
|
||||
{
|
||||
meow_view *cur = g_top_view;
|
||||
meow_view *next = view_map_get(id);
|
||||
if (cur && cur != next) view_leave(cur);
|
||||
view_enter(next);
|
||||
g_top_view = next ? next : cur;
|
||||
}
|
||||
|
||||
/* ---- 页内子导航(预留扩展位 a 桩:本批不实现,仅返回占位)---- */
|
||||
int meow_view_sub_push(meow_view *v, meow_view *sub)
|
||||
{
|
||||
(void)v; (void)sub;
|
||||
return 0; /* 预留:将来在 ACTIVITY view 内维护子栈 */
|
||||
}
|
||||
meow_view *meow_view_sub_pop(meow_view *v)
|
||||
{
|
||||
(void)v;
|
||||
return NULL; /* 预留 */
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 事件路由(§A.7:每个 view 的 on_event 先把事件发给后代(按 z 降序),命中即消费,
|
||||
* 再落本 view 的控件)。SDK 提供递归树走;叶子 on_event 负责本 view 的 widget 命中。 */
|
||||
/* ============================================================ */
|
||||
static int view_dispatch_event(meow_view *v, int ev, int a, int b)
|
||||
{
|
||||
if (!v || !(v->state & MEOW_VIEW_STATE_ENTERED)) return 0; /* 仅已进入的视图参与 */
|
||||
if (v->nchildren > 0) {
|
||||
meow_view *kids[MEOW_VIEW_MAX_CHILD];
|
||||
int nn = v->nchildren;
|
||||
for (int i = 0; i < nn; i++) kids[i] = v->children[i];
|
||||
/* 按 z 降序(同 z 保持挂载序稳定);z 同值时树序,靠后挂的在先。 */
|
||||
for (int i = 1; i < nn; i++) {
|
||||
meow_view *key = kids[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && meow_view_z(kids[j]) < meow_view_z(key)) { kids[j + 1] = kids[j]; j--; }
|
||||
kids[j + 1] = key;
|
||||
}
|
||||
for (int i = 0; i < nn; i++)
|
||||
if (view_dispatch_event(kids[i], ev, a, b)) return 1; /* 子命中即消费 */
|
||||
}
|
||||
if (v->hooks.on_event && v->hooks.on_event(v, ev, a, b, v->ctx)) return 1; /* 落到本 view 事件 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_view_dispatch_event(int ev, int a, int b)
|
||||
{
|
||||
return view_dispatch_event(g_top_view, ev, a, b);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 坐标 / 查询 */
|
||||
/* ============================================================ */
|
||||
int meow_view_screen_x(const meow_view *v)
|
||||
{
|
||||
int x = v ? v->x : 0;
|
||||
const meow_view *p = v ? v->parent : NULL;
|
||||
while (p) { x += p->x; p = p->parent; }
|
||||
return x;
|
||||
}
|
||||
int meow_view_screen_y(const meow_view *v)
|
||||
{
|
||||
int y = v ? v->y : 0;
|
||||
const meow_view *p = v ? v->parent : NULL;
|
||||
while (p) { y += p->y; p = p->parent; }
|
||||
return y;
|
||||
}
|
||||
|
||||
meow_view *meow_view_parent(const meow_view *v) { return v ? v->parent : NULL; }
|
||||
int meow_view_get_state(const meow_view *v) { return v ? (int)v->state : 0; }
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* meow_ui_view.h — SDK 视图/页面框架(meow_view,嵌套容器 + 生命周期 + 事件路由)。
|
||||
*
|
||||
* 源:devdocs/plan/SDK_VIEW_FRAMEWORK_DESIGN.md §A。本文件为机制 ABI(结构 + 生命周期钩子 +
|
||||
* 页面导航包装),与 z 序绘制物注册表(meow_ui_draw.c,§C/§6.2)协同:
|
||||
* - meow_view 是**薄壳**:容器 + 生命周期 + 事件路由;叶子仍是 meow_widget(不改)。
|
||||
* - on_draw 是回调,view 不缓存像素(非保留);重绘由 meow_ui_invalidate 触发。
|
||||
* - opt-in:不调用 meow_view_* 的 App 完全照旧(widget/activity/flush 不动)。
|
||||
* - z 深度源 = view 树(parent/children 深度);叶子 widget 深度 = 所属 view 深度 + 1。
|
||||
*
|
||||
* 页面模型(定案 b,预留 a):顶层页切换仍由 activity 栈(meow_view_page_push/pop/replace 包装),
|
||||
* 不做页内子导航栈;meow_view_sub_push/pop 为将来扩展位(仅在一个 ACTIVITY view 内维护子栈)。
|
||||
*/
|
||||
#ifndef MEOW_UI_VIEW_H
|
||||
#define MEOW_UI_VIEW_H
|
||||
|
||||
#include "meow_ui.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct meow_view meow_view;
|
||||
|
||||
typedef enum { MEOW_VIEW_ACTIVITY = 0, MEOW_VIEW_CONTAINER = 1 } meow_view_type;
|
||||
typedef enum {
|
||||
MEOW_VIEW_STATE_CREATED = 1u << 0,
|
||||
MEOW_VIEW_STATE_ENTERED = 1u << 1,
|
||||
MEOW_VIEW_STATE_DIRTY = 1u << 2,
|
||||
} meow_view_state;
|
||||
|
||||
/* 生命周期/事件钩子(on_create→on_enter→(事件循环)→on_leave→on_destroy + on_layout)。
|
||||
* 均以 view 自身 + ctx(=视图控制器,业务-UI 绑定点)为参。可 NULL。 */
|
||||
typedef void (*meow_view_fn)(meow_view *v, void *ctx);
|
||||
typedef int (*meow_view_ev_fn)(meow_view *v, int ev, int a, int b, void *ctx);
|
||||
typedef void (*meow_view_draw_fn)(meow_view *v, const meow_theme *th, void *ctx);
|
||||
|
||||
typedef struct meow_view_hooks {
|
||||
meow_view_fn on_create; /* 建 view 后跑一次(分配视图态/初始布局) */
|
||||
meow_view_fn on_enter; /* 页面进入(push/replace 后;回页面自动恢复零重建) */
|
||||
meow_view_fn on_leave; /* 页面离开(pop/replace 前;默认不销毁子 view/widget) */
|
||||
meow_view_fn on_destroy; /* meow_view_free 释放(递归)时跑 */
|
||||
meow_view_fn on_layout; /* 布局(on_create/page_push 后跑一次,算子控件绝对坐标) */
|
||||
meow_view_draw_fn on_draw; /* 绘制回调(非保留;由 meow_ui_invalidate 触发) */
|
||||
meow_view_ev_fn on_event; /* 事件路由(可 NULL;先发后代按 z 降序,命中即消费) */
|
||||
} meow_view_hooks;
|
||||
|
||||
#define MEOW_VIEW_MAX_CHILD 16
|
||||
#define MEOW_VIEW_MAX_WIDGET 32
|
||||
|
||||
struct meow_view {
|
||||
int type; /* MEOW_VIEW_ACTIVITY / MEOW_VIEW_CONTAINER */
|
||||
int id; /* 页面/视图 id(App 命名的稳定值;= page_push 用的 id) */
|
||||
int x, y, w, h; /* 自身坐标(相对父内容原点;子 view 默认占满,叠放由 z 排) */
|
||||
meow_view *parent;
|
||||
meow_view *children[MEOW_VIEW_MAX_CHILD];
|
||||
int nchildren;
|
||||
meow_widget *widgets[MEOW_VIEW_MAX_WIDGET]; /* 叶子控件(直接挂本 view) */
|
||||
int nwidgets;
|
||||
int z; /* 显式 z(meow_view_set_z);0=未设(由子/注册表推导) */
|
||||
unsigned state; /* meow_view_state 位掩码 */
|
||||
meow_view_hooks hooks;
|
||||
void *ctx; /* 视图控制器(biz-UI 绑定点) */
|
||||
};
|
||||
|
||||
/* ---- 生命周期 ---- */
|
||||
meow_view *meow_view_create(int id, meow_view_type type,
|
||||
const meow_view_hooks *hooks, void *ctx);
|
||||
void meow_view_free(meow_view *v); /* 递归 on_destroy + widget_free 叶子 + 注销 z 序 */
|
||||
|
||||
/* ---- 嵌套(组合视图树)---- */
|
||||
int meow_view_add_child(meow_view *parent, meow_view *child); /* 任意深挂子 view;0/负=结果 */
|
||||
int meow_view_add_widget(meow_view *v, meow_widget *w); /* 挂叶子控件(登记 z 深度源) */
|
||||
void meow_view_set_z(meow_view *v, int z); /* 显式 z(999 置顶);影响其绘制物 z */
|
||||
int meow_view_z(const meow_view *v);
|
||||
|
||||
/* ---- 页面导航(包装 activity 栈,唯一驱动 on_enter/on_leave——单一入口,App 不再分别调)----
|
||||
* 顶层页切换走 activity 栈;页内子导航为预留(见设计定案 b)。 */
|
||||
void meow_view_page_push(int id); /* activity_push + 旧top on_leave + 新页 on_enter */
|
||||
void meow_view_page_pop(void); /* 旧top on_leave + activity_pop + 新top on_enter */
|
||||
void meow_view_page_replace(int id); /* 旧top on_leave + 新页 on_enter(目标页须已 create) */
|
||||
|
||||
/* 入口:把语义事件注入当前活动页 view 子树路由——先发子视图(按 z 降序),命中即消费,
|
||||
* 再落本 view 的 on_event(其内做该 view 的 widget 命中)。返回 1=已消费(§A.7)。
|
||||
* 未接线于 meow_ui_run(App 可在 on_tap/on_swipe 等回调里改用它实现 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(未实现于本批) */
|
||||
|
||||
/* ---- 坐标:view 相对父内容原点的累计(父坐标链求和 → 屏幕绝对坐标)---- */
|
||||
int meow_view_screen_x(const meow_view *v);
|
||||
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_* 位掩码 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_UI_VIEW_H */
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* meow_ui_vol.c — SDK 状态栏音量换算纯函数 + settings.conf 读取(A-0 核心数源)。
|
||||
*
|
||||
* 单独成 TU 的目的:
|
||||
* 1. 纯函数(clamp / 键读取)不依赖 meow_ui 其它子系统,宿主 gcc 直接可测
|
||||
* (test_meow_ui_vol.c),无需链整库/stub。
|
||||
* 2. 把「音量从 settings 读数并 clamp 到 volume_max」这一语义钉死在单点,
|
||||
* 与 launcher 的 vol_clamp(audio.c)口径对齐,杜绝两端音量漂移。
|
||||
*
|
||||
* 数据源:/usr/data/meow/settings.conf 扁平 key=value。launcher meow_settings.c
|
||||
* 落盘为 "key = value"(带空格),本解析兼容无空格/多空格,与 launcher 读循环
|
||||
* 及 sdk meow_ui_theme.c 的 get_setting_str 同款解析。
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int meow_ui_vol_clamp(int vol, int vol_max)
|
||||
{
|
||||
if (vol < 0) vol = 0;
|
||||
if (vol > MEOW_UI_VOL_MAX) vol = MEOW_UI_VOL_MAX;
|
||||
if (vol_max < 0) vol_max = 0; /* 异常上限兜底:不进负值 */
|
||||
if (vol > vol_max) vol = vol_max;
|
||||
return vol;
|
||||
}
|
||||
|
||||
int meow_ui_vol_read(const char *path, const char *key, int def)
|
||||
{
|
||||
if (!path || !key) return def;
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return def;
|
||||
int kl = (int)strlen(key);
|
||||
char line[160];
|
||||
char *found = NULL;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue; /* 非 key=value 行(注释/空行)跳过 */
|
||||
char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++; /* key 左去空白 */
|
||||
int n = (int)(eq - k);
|
||||
while (n > 0 && (k[n-1] == ' ' || k[n-1] == '\t')) n--; /* key 右去空白 */
|
||||
if (n != kl || strncmp(k, key, (size_t)kl) != 0) continue; /* key 不精确匹配 */
|
||||
found = eq + 1; /* 命中所求键 */
|
||||
break;
|
||||
}
|
||||
fclose(f);
|
||||
if (!found) return def;
|
||||
while (*found == ' ' || *found == '\t') found++; /* value 左去空白;atoi 天然停于非数字 */
|
||||
return atoi(found);
|
||||
}
|
||||
|
||||
/* 改 settings.conf 一个 int 键:流式拷贝原文件到 <path>.tmp,命中键行改写为
|
||||
* "key = value"(launcher 落盘格式,带空格),键缺失则末尾追加;rename 原子替换。
|
||||
* 保留其余行(注释/其它设置)——绝不 O_TRUNC 整文件。 */
|
||||
int meow_ui_vol_set(const char *path, const char *key, int val)
|
||||
{
|
||||
if (!path || !path[0] || !key || !key[0]) return -1;
|
||||
FILE *in = fopen(path, "r");
|
||||
char tmp[512];
|
||||
int kl = (int)strlen(key);
|
||||
snprintf(tmp, sizeof tmp, "%s.tmp", path);
|
||||
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 = %d\n", key, val); /* 命中:改写为规范格式 */
|
||||
found = 1;
|
||||
} else {
|
||||
fputs(line, out); /* 其它行原样保留 */
|
||||
}
|
||||
}
|
||||
if (in) fclose(in);
|
||||
if (!found) fprintf(out, "%s = %d\n", key, val);
|
||||
fclose(out);
|
||||
|
||||
if (rename(tmp, path) != 0) { remove(tmp); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 音量步进(系统语义,与 launcher 设置页/watchdog 同源:volume_step/volume_max 约束)。
|
||||
* 只做 settings 逻辑 + 持久化;DAC 写入由调用方(meow_ui_vol_dac_write)负责。 */
|
||||
int meow_ui_vol_step(const char *path, int dir)
|
||||
{
|
||||
int vol = meow_ui_vol_read(path, "audio.volume", MEOW_UI_VOL_DEFAULT);
|
||||
int vol_max= meow_ui_vol_read(path, "audio.volume_max", MEOW_UI_VOL_MAX_DEF);
|
||||
int step = meow_ui_vol_read(path, "audio.volume_step", 1);
|
||||
if (step < 1) step = 1;
|
||||
if (step > 5) step = 5; /* 与 watchdog 读取口径一致(1..5) */
|
||||
if (dir < 0) dir = -1;
|
||||
else if (dir > 0) dir = 1;
|
||||
else return vol;
|
||||
|
||||
int v = meow_ui_vol_clamp(vol + dir * step, vol_max);
|
||||
if (v == vol) return vol; /* 已在上下限:无进展,不写 */
|
||||
meow_ui_vol_set(path, "audio.volume", v);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* 真实 DAC 音量(CS43198, numid 1/2, 0-255),pct→255 映射直写两声道。
|
||||
* 与 launcher meow_dac_vol_write / watchdog wd_vol_write 完全同路径(语义同源)。 */
|
||||
void meow_ui_vol_dac_write(int pct)
|
||||
{
|
||||
int t = 100 - pct, dac;
|
||||
if (pct <= 0) dac = 255; /* 真静音(DS1039:255=-127.5dB) */
|
||||
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
||||
if (dac > 255) dac = 255; if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"amixer -c 0 cset numid=1 %d >/dev/null 2>&1; amixer -c 0 cset numid=2 %d >/dev/null 2>&1",
|
||||
dac, dac);
|
||||
system(cmd);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* meow_ui_vol.h — SDK 状态栏音量换算/读取(meow_ui_vol.c)。
|
||||
* 不随 meow_ui.h 发布给应用;仅供 meow_ui_sys.c 与宿主单测(test_meow_ui_vol.c)使用。
|
||||
*/
|
||||
#ifndef MEOW_UI_VOL_H
|
||||
#define MEOW_UI_VOL_H
|
||||
|
||||
/* 音量语义常量(与 launcher MEOW_VOL_MAX/DEFAULT、meow_settings 表逐一致):
|
||||
* audio.volume 默认 60,取值域 0-100(硬上限 MEOW_UI_VOL_MAX);
|
||||
* audio.volume_max 默认 100(软上限,clamp 上限,用户可调小);
|
||||
* audio.volume_step 默认 1(跳步;只影响「每次音量键改多少」,与状态栏展示无关——
|
||||
* SDK 只读当前值,步进由改动方(launcher)承担)。 */
|
||||
#define MEOW_UI_VOL_DEFAULT 60
|
||||
#define MEOW_UI_VOL_MAX 100
|
||||
#define MEOW_UI_VOL_MAX_DEF 100
|
||||
|
||||
/* 纯函数:把软件音量夹到 [0, MEOW_UI_VOL_MAX],再夹到 volume_max 软上限。
|
||||
* 语义与 launcher 的 vol_clamp(audio.c)一致:
|
||||
* - 硬上限 100(0-100 口径);
|
||||
* - 尊重 audio.volume_max(用户把上限调小则音量被夹到该值);
|
||||
* - volume_max 异常(<0)兜底为 0(不进负值)。
|
||||
* 无文件/全局依赖,宿主 gcc 可直接测。 */
|
||||
int meow_ui_vol_clamp(int vol, int vol_max);
|
||||
|
||||
/* 读 settings.conf 里一个 int 键。扁平 "key=value" 格式(launcher meow_settings.c
|
||||
* 落盘为 "key = value",带空格;这里兼容无空格/多空格两种写式)。
|
||||
* 解析:'=' 切分,key/value 去首尾空白,key 精确匹配(避免 audio.volume 误配
|
||||
* audio.volume_max)。未命中/打开失败/键缺失回退 def。
|
||||
* path 由调用方传入(设备传导 MEOW_SETTINGS_FILE;宿主单测指向临时文件)。 */
|
||||
int meow_ui_vol_read(const char *path, const char *key, int def);
|
||||
|
||||
/* 改 settings.conf 里一个 int 键(地址更新/新增;**保留其余行**——与 launcher 落盘格式
|
||||
* "key = value"(带空格)一致;键缺失则追加)。用临时文件 + rename 原子替换,失败返回 -1。
|
||||
* 用途:SDK 输入层调系统音量时写 audio.volume(绝不像 watchdog wd_vol_save 那样整文件
|
||||
* O_TRUNC,避免删掉 display.* 等其它设置)。 */
|
||||
int meow_ui_vol_set(const char *path, const char *key, int val);
|
||||
|
||||
/* 读 audio.volume + audio.volume_max + audio.volume_step,按 dir(±1)迈一步(绝对跨度
|
||||
* = volume_step),clamp 到 [0, MEOW_UI_VOL_MAX] 再 clamp 到 volume_max(同 meow_ui_vol_clamp
|
||||
* 语义,与 launcher 设置页/watchdog 一致),经 meow_ui_vol_set 持久化到 audio.volume。
|
||||
* 返回写入 DAC 的映射值(= 夹后新卷值);已在上下限未变则返回当前值(不写)。
|
||||
* 仅做 settings 逻辑(不改 DAC);DAC 写入由调用方用 meow_ui_vol_dac_write 做。 */
|
||||
int meow_ui_vol_step(const char *path, int dir);
|
||||
|
||||
/* 写真实 DAC(CS43198, numid 1/2, pct→0-255),与 launcher meow_dac_vol_write / watchdog
|
||||
* wd_vol_write 同路径(amixer cset = SNDRV_CTL_IOCTL_ELEM_WRITE)。无副作用前提:宿主上
|
||||
* amixer 不存在 → system() 静默失败(拼音量的宿主单测只测 meow_ui_vol_step,不测写 DAC)。 */
|
||||
void meow_ui_vol_dac_write(int pct);
|
||||
|
||||
#endif /* MEOW_UI_VOL_H */
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,51 @@
|
||||
/* render_sb_statusbar.c — 宿主可视化渲染:把「同一个状态栏」画进 back_buf 并 dump PPM 目检。
|
||||
* 仅用于宿主验证(无 /dev/fb0 → 480x720 离屏;字体缺失时图标仍画,数值可能缺字)。
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o render_sb_statusbar render_sb_statusbar.c meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c \
|
||||
* meow_ui_vol.c meow_ui_log.c /tmp/meow_font_host.o -I. -I../meow_launcherd -lz
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
meow_ui_sys_set_system_mode(1);
|
||||
meow_ui_set_statusbar(1);
|
||||
|
||||
/* 喂权威快照(模拟 launcher):电量81/充电/音量60/蓝牙连/WiFi已连-50(强)/亮度100 */
|
||||
meow_ui_feed f;
|
||||
memset(&f, 0, sizeof f);
|
||||
f.batt = 81; f.charging = 1; f.wifi_on = 1; f.wifi_connected = 1; f.wifi_level = -50;
|
||||
f.bt_seen = 1; f.bt_on = 1; f.vol = 60; f.brightness = 100;
|
||||
meow_ui_sys_feed(&f);
|
||||
|
||||
/* 清整屏(背景)→ 画状态栏 → 把 top 28 行 + 一点内容区 dump 成 PPM */
|
||||
meow_ui_clear(0x141824);
|
||||
meow_ui_sys_statusbar_render();
|
||||
|
||||
const char *path = argv[1] ? argv[1] : "/tmp/sb_statusbar.ppm";
|
||||
FILE *fp = fopen(path, "wb");
|
||||
if (!fp) { printf("cannot open %s\n", path); return 1; }
|
||||
int w = 480, h = 480; /* 宽 x 全屏高,看顶条+背景 */
|
||||
fprintf(fp, "P6\n%d %d\n255\n", w, h);
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
for (int ry = 0; ry < h; ry++) {
|
||||
for (int cx = 0; cx < w; cx++) {
|
||||
uint16_t p = *(uint16_t *)(bb + (size_t)ry * (size_t)(w * 2) + (size_t)cx * 2);
|
||||
int r = ((p >> 11) & 0x1F) << 3;
|
||||
int g = ((p >> 5) & 0x3F) << 2;
|
||||
int b = (p & 0x1F) << 3;
|
||||
fputc(r, fp); fputc(g, fp); fputc(b, fp);
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
printf("wrote %s (%dx%d)\n", path, w, h);
|
||||
return 0;
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/* sb_icons.h — 状态栏/通知中心图标(Material Symbols Rounded, MD3),单通道 alpha 位图 32x32。
|
||||
* 生成自 material-symbols@0.47.0 rounded 字体光栅化;每像素 0..255(亮度/alpha)。
|
||||
* ★ 为日/夜间预留:位图只存亮度;绘制颜色由调用方主题表提供,换主题仅改配色。 */
|
||||
#ifndef SB_ICONS_H
|
||||
#define SB_ICONS_H
|
||||
|
||||
#define SB_ICON_SIZE 32
|
||||
|
||||
typedef struct { const unsigned char *data; } sb_icon_t;
|
||||
|
||||
static const unsigned char SB_ICON_WIFI[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,2,3,4,3,3,2,2,3,3,4,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,30,77,122,154,177,187,187,177,154,122,77,30,0,0,0,3,1,0,0,0,0,0,0,0,0,0,3,0,0,25,108,191,244,255,255,255,255,255,255,255,255,255,255,244,191,108,25,0,0,3,0,0,0,0,0,0,1,1,0,27,140,235,255,255,255,249,248,251,254,255,255,254,251,248,249,255,255,255,235,140,27,0,1,1,0,0,0,2,0,0,110,235,255,255,249,251,255,255,255,255,246,240,240,246,255,255,255,255,251,249,255,255,235,110,0,0,2,0,1,0,25,181,255,255,249,253,255,255,216,151,92,53,29,18,18,29,53,92,151,217,255,255,253,249,255,255,181,25,0,1,
|
||||
1,30,220,255,250,251,255,255,180,74,6,0,0,0,0,0,0,0,0,0,0,6,75,180,255,255,251,250,255,220,30,1,0,124,255,244,252,255,207,69,0,0,0,6,7,4,0,0,0,0,4,7,6,0,0,0,69,207,255,252,244,255,123,0,0,76,255,255,255,147,8,0,3,6,0,0,0,0,7,15,15,7,0,0,0,0,6,3,0,8,147,255,255,255,76,0,1,0,93,151,80,0,0,5,1,0,2,67,140,193,221,235,235,221,192,138,64,0,0,1,5,0,0,80,151,93,0,1,0,0,0,0,0,2,2,0,0,97,207,255,255,255,255,255,255,255,255,255,255,204,93,0,0,2,2,0,0,0,0,0,0,0,3,4,3,2,0,37,191,255,255,251,248,255,255,255,255,255,254,248,251,255,255,187,35,0,2,3,4,3,0,0,0,0,0,0,1,1,7,212,255,248,251,255,255,246,213,193,193,214,247,255,255,250,248,255,211,7,1,1,0,0,0,0,0,0,0,0,1,0,10,235,255,255,255,197,98,31,3,0,0,3,33,100,200,255,255,255,234,10,0,1,0,0,0,0,
|
||||
0,0,0,0,0,2,0,85,216,206,95,0,0,0,1,6,6,1,0,0,0,99,208,215,81,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,4,3,0,0,0,0,3,3,0,0,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,3,0,1,0,48,119,107,25,0,1,0,3,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,2,0,95,251,255,255,230,48,0,2,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,21,241,255,249,248,255,190,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,63,255,252,255,254,253,235,14,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,46,255,251,253,251,252,225,7,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,185,255,255,255,255,124,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,0,20,163,231,223,128,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,13,8,0,0,0,0,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,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,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,0,0,0,0,0,0,0,0,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_BT[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,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,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,1,1,9,153,156,6,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,3,0,61,255,255,183,8,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,3,0,66,252,247,255,186,10,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,2,3,0,65,255,253,255,255,188,11,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,44,221,195,23,0,6,0,65,255,251,227,255,255,191,13,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,85,255,255,208,23,0,1,65,255,254,69,167,255,255,193,16,2,1,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,2,5,159,255,255,209,25,0,69,255,255,63,0,175,252,255,165,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,160,255,255,212,17,55,255,255,57,16,197,251,255,150,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,159,255,255,198,88,254,254,88,198,255,255,171,6,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,158,255,255,244,254,254,244,255,255,167,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,158,255,255,255,255,255,255,164,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,153,251,253,253,251,156,1,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,18,235,254,254,236,19,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,21,204,254,253,253,253,204,20,0,3,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,3,0,20,204,255,255,255,255,255,255,203,19,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,21,205,255,255,214,254,254,214,255,255,203,19,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,21,206,255,255,151,60,255,255,60,151,255,255,203,21,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,22,206,255,255,167,0,63,255,255,66,0,169,253,255,170,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,1,21,203,255,255,163,1,1,67,255,255,54,25,206,251,255,148,0,3,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,255,162,0,4,0,65,255,253,101,211,255,255,176,6,2,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,22,169,140,0,1,5,0,65,255,252,252,255,255,178,5,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,65,255,253,251,255,180,5,1,2,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,1,3,3,1,0,3,0,66,252,247,255,182,6,1,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,60,255,255,183,7,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,1,8,151,157,7,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,3,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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_BATT[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,1,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,14,16,16,14,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,7,4,75,235,237,237,235,75,4,7,4,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,124,255,255,255,255,124,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,80,123,115,184,253,251,251,253,184,116,123,98,8,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,49,255,255,255,255,255,255,255,255,255,255,255,255,107,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,68,253,249,239,236,239,238,238,239,236,236,246,252,131,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,64,255,255,77,16,19,17,17,17,16,32,235,255,125,0,4,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,3,0,65,255,255,62,0,0,0,0,0,0,13,234,255,126,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,255,66,1,4,1,1,2,1,18,234,255,126,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,255,67,2,5,2,2,3,2,19,234,255,126,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,255,56,0,0,0,0,0,0,6,233,255,126,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,254,206,190,190,190,190,190,190,194,247,255,127,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,252,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,253,252,252,252,252,252,252,252,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,65,255,253,255,255,255,255,255,255,255,255,251,255,128,0,4,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,3,0,64,255,253,255,255,255,255,255,255,255,255,251,255,127,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,67,253,249,251,251,251,251,251,251,251,251,247,251,131,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,51,255,255,255,255,255,255,255,255,255,255,255,255,111,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,88,128,126,128,128,128,128,128,128,127,127,110,11,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,3,4,4,4,4,4,4,4,4,4,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
};
|
||||
|
||||
static const unsigned char SB_ICON_VOL[1024]={
|
||||
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,1,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,0,0,0,0,2,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,2,1,1,20,193,209,94,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,2,0,48,255,255,255,178,21,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,53,198,127,1,3,2,104,229,255,255,211,24,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,58,239,255,245,15,0,2,0,23,172,255,255,199,7,3,1,0,0,0,0,0,0,0,0,0,3,3,3,3,5,0,57,239,255,252,236,17,0,0,3,0,0,161,255,255,131,0,3,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,58,239,255,255,255,238,17,0,4,3,0,3,5,206,254,245,32,0,2,0,0,0,0,0,0,0,0,51,66,65,66,88,238,255,255,227,255,238,20,0,110,209,24,3,0,64,253,255,130,0,4,0,0,0,0,0,3,0,87,255,255,255,255,255,255,255,111,117,255,234,21,0,130,255,187,2,2,0,198,254,205,2,1,0,0,0,0,0,4,0,133,255,249,255,255,255,254,122,0,127,255,234,21,0,128,253,255,78,0,0,125,255,242,28,0,2,0,0,0,0,4,0,125,255,238,72,58,62,49,0,2,127,255,234,21,0,129,255,254,159,0,0,83,255,255,55,0,3,0,0,0,0,4,0,126,255,233,2,0,0,0,9,0,126,255,234,21,0,129,255,255,187,0,0,67,255,255,64,0,3,0,0,0,0,4,0,125,255,235,31,15,17,9,0,2,126,255,234,21,0,129,255,254,173,0,0,76,255,255,59,0,3,0,0,0,0,4,0,131,251,245,228,226,227,220,71,0,131,255,234,21,0,129,254,255,108,0,1,112,255,247,36,0,2,0,0,
|
||||
0,0,4,0,111,255,255,255,255,255,255,249,61,111,255,234,21,0,127,255,225,17,1,0,178,255,217,6,0,1,0,0,0,0,1,0,11,110,127,126,126,148,255,255,240,197,255,237,21,0,127,249,61,0,2,37,245,255,153,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,106,255,255,255,255,238,18,0,25,41,0,6,0,173,255,254,54,0,3,0,0,0,0,0,0,0,1,4,4,4,4,5,0,106,255,250,254,236,17,0,0,0,5,0,112,255,255,169,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,106,255,255,245,17,0,3,0,0,117,254,255,232,26,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,102,243,176,3,3,0,53,184,255,255,244,60,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,26,4,1,1,38,251,255,255,224,60,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,35,238,246,153,23,0,3,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,1,0,31,39,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,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
|
||||
};
|
||||
|
||||
static const unsigned char SB_ICON_BRIGHT[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,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,3,0,14,64,64,32,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,4,4,6,0,36,212,255,255,240,77,0,5,4,4,4,3,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,47,226,255,255,254,255,251,91,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,67,125,126,127,131,234,255,255,118,73,248,255,253,147,126,126,126,89,2,0,1,0,0,0,0,0,0,0,0,3,0,106,255,255,255,255,255,255,255,119,0,0,71,243,255,255,255,255,255,255,162,2,2,0,0,0,0,0,0,0,1,0,7,227,254,237,226,227,225,228,111,0,8,9,0,64,218,227,226,226,232,252,255,46,0,2,0,0,0,0,0,0,1,0,17,234,255,132,12,16,13,14,0,0,0,0,0,0,9,14,15,12,74,255,254,67,0,3,0,0,0,
|
||||
0,0,0,1,0,18,234,255,124,0,1,0,0,15,77,119,124,89,31,0,0,1,0,63,255,255,66,0,3,0,0,0,0,0,0,1,1,14,233,255,126,1,7,0,98,224,255,255,255,255,240,135,5,4,1,65,255,255,61,2,3,0,0,0,0,0,0,3,0,30,241,253,130,0,0,125,255,255,252,251,251,252,255,255,177,6,1,69,254,255,85,0,5,0,0,0,0,0,2,0,21,204,253,255,116,0,78,255,251,253,255,255,255,255,254,249,255,137,0,57,255,253,234,58,0,3,0,0,0,1,0,23,206,255,255,169,10,3,203,254,252,255,255,255,255,255,255,254,252,246,29,0,118,255,255,240,59,0,2,0,0,2,5,194,255,255,173,2,0,37,251,255,254,255,255,255,255,255,255,255,252,255,92,0,0,119,255,255,238,31,1,1,3,0,57,255,253,176,0,6,1,63,255,253,255,255,255,255,255,255,255,255,251,254,124,0,9,0,120,253,255,119,0,5,2,0,45,255,253,204,28,0,1,59,255,254,255,255,255,255,255,255,255,255,251,254,120,1,5,5,162,253,255,102,0,4,
|
||||
0,2,0,151,255,255,216,27,0,27,245,254,254,255,255,255,255,255,255,255,252,255,78,0,3,171,255,255,204,13,1,1,0,1,2,1,160,255,255,213,33,0,179,255,251,255,255,255,255,255,255,253,253,231,16,10,169,255,255,206,22,0,1,0,0,0,1,2,0,158,252,255,131,0,47,246,255,251,254,255,255,254,252,251,255,96,0,67,255,252,202,21,0,2,0,0,0,0,0,2,0,12,236,253,126,0,0,76,244,255,255,253,253,255,255,255,126,0,1,66,253,255,64,0,5,0,0,0,0,0,0,1,1,18,233,255,128,4,11,0,50,175,241,255,255,248,198,81,0,9,4,68,255,255,65,1,3,0,0,0,0,0,0,1,0,18,233,255,117,0,0,0,0,0,28,59,63,37,0,0,0,0,0,51,255,254,65,0,3,0,0,0,0,0,0,1,0,15,237,252,184,116,119,117,118,24,0,0,0,0,0,107,119,118,116,152,253,255,65,0,3,0,0,0,0,0,0,0,2,2,192,255,255,255,255,255,255,208,26,0,6,3,163,255,255,255,255,255,255,241,24,0,1,0,0,0,
|
||||
0,0,0,0,2,0,34,187,234,234,234,237,255,255,212,24,0,169,255,255,243,234,234,234,209,70,0,2,0,0,0,0,0,0,0,0,0,0,0,0,16,17,18,20,159,255,255,207,175,255,255,205,35,18,17,17,1,0,1,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,159,255,255,255,255,206,21,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,2,2,0,151,252,255,190,22,0,3,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,45,56,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,2,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,2,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
|
||||
};
|
||||
|
||||
static const unsigned char SB_ICON_WIFI_STRONG[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,2,3,4,3,3,2,2,3,3,4,3,2,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,4,1,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,1,3,0,0,0,30,77,122,154,177,187,187,177,154,122,77,30,0,0,0,3,1,0,0,0,0,0,
|
||||
0,0,0,0,3,0,0,25,108,191,244,255,255,255,255,255,255,255,255,255,255,244,191,108,25,0,0,3,0,0,0,0,
|
||||
0,0,1,1,0,27,140,235,255,255,255,249,248,251,254,255,255,254,251,248,249,255,255,255,235,140,27,0,1,1,0,0,
|
||||
0,2,0,0,110,235,255,255,249,251,255,255,255,255,246,240,240,246,255,255,255,255,251,249,255,255,235,110,0,0,2,0,
|
||||
1,0,25,181,255,255,249,253,255,255,216,151,92,53,29,18,18,29,53,92,151,217,255,255,253,249,255,255,181,25,0,1,
|
||||
1,30,220,255,250,251,255,255,180,74,6,0,0,0,0,0,0,0,0,0,0,6,75,180,255,255,251,250,255,220,30,1,
|
||||
0,124,255,244,252,255,207,69,0,0,0,6,7,4,0,0,0,0,4,7,6,0,0,0,69,207,255,252,244,255,123,0,
|
||||
0,76,255,255,255,147,8,0,3,6,0,0,0,0,7,15,15,7,0,0,0,0,6,3,0,8,147,255,255,255,76,0,
|
||||
1,0,93,151,80,0,0,5,1,0,2,67,140,193,221,235,235,221,192,138,64,0,0,1,5,0,0,80,151,93,0,1,
|
||||
0,0,0,0,0,2,2,0,0,97,207,255,255,255,255,255,255,255,255,255,255,204,93,0,0,2,2,0,0,0,0,0,
|
||||
0,0,3,4,3,2,0,37,191,255,255,251,248,255,255,255,255,255,254,248,251,255,255,187,35,0,2,3,4,3,0,0,
|
||||
0,0,0,0,1,1,7,212,255,248,251,255,255,246,213,193,193,214,247,255,255,250,248,255,211,7,1,1,0,0,0,0,
|
||||
0,0,0,0,1,0,10,235,255,255,255,197,98,31,3,0,0,3,33,100,200,255,255,255,234,10,0,1,0,0,0,0,
|
||||
0,0,0,0,0,2,0,85,216,206,95,0,0,0,1,6,6,1,0,0,0,99,208,215,81,0,2,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,4,0,0,0,4,3,0,0,0,0,3,3,0,0,1,3,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,2,0,0,3,0,1,0,48,119,107,25,0,1,0,3,0,0,2,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,1,1,0,2,0,95,251,255,255,230,48,0,2,0,1,1,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,1,0,21,241,255,249,248,255,190,1,2,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,3,0,63,255,252,255,254,253,235,14,0,1,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,2,0,46,255,251,253,251,252,225,7,0,1,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,2,2,185,255,255,255,255,124,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,0,20,163,231,223,128,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,13,8,0,0,0,0,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,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,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,
|
||||
0,0,0,0,0,0,0,0,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_WIFI_MEDIUM[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,1,3,4,2,2,2,2,2,2,4,3,1,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,0,0,0,0,3,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,2,0,0,3,68,141,187,189,189,189,189,186,139,65,1,0,1,2,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,1,0,1,99,209,255,255,255,255,255,255,255,255,255,255,205,95,0,0,1,0,0,0,0,0,0,
|
||||
0,0,0,0,0,2,0,39,194,255,255,251,248,255,255,255,255,255,254,248,251,255,255,190,37,0,2,0,0,0,0,0,
|
||||
0,0,0,0,1,1,7,215,255,248,251,255,255,246,213,193,193,213,247,255,255,251,248,255,213,7,1,1,0,0,0,0,
|
||||
0,0,0,0,1,0,10,233,255,255,255,194,96,31,3,0,0,3,32,99,197,255,255,255,232,9,0,1,0,0,0,0,
|
||||
0,0,0,0,0,2,0,80,210,199,89,0,0,0,1,6,6,1,0,0,0,93,202,209,76,0,2,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,1,3,3,0,0,0,0,4,4,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,2,0,0,3,0,1,0,25,107,119,47,0,1,0,3,0,0,2,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,1,0,0,2,0,48,230,255,255,251,94,0,2,0,0,1,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,2,1,191,255,248,249,255,241,20,0,1,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,1,0,14,236,253,254,255,252,255,63,0,3,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,1,0,7,226,252,251,253,251,255,46,0,2,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,3,0,124,255,255,255,255,184,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,1,2,128,223,231,162,19,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,8,13,0,0,0,0,0,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,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,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,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,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_WIFI_WEAK[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,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,2,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,1,0,4,17,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,2,0,52,213,237,238,184,19,1,1,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,1,1,10,219,255,255,255,255,160,0,3,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,3,0,57,255,252,254,252,252,232,11,0,1,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,3,0,57,255,252,255,253,252,232,11,0,1,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,1,1,10,219,255,251,251,255,160,0,3,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,2,0,52,215,255,255,185,20,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,1,0,9,59,50,0,0,0,0,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,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,1,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,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
};
|
||||
|
||||
/* 网络状态位图(M2.48):WiFi 开未连接/关闭、蓝牙 未连接/已连接/关闭/搜索中 */
|
||||
static const unsigned char SB_ICON_WIFI_NO[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,2,3,4,3,3,2,2,3,3,4,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,4,1,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,0,0,30,77,122,154,177,187,187,177,154,122,77,30,0,0,0,3,1,0,0,0,0,0,0,0,0,0,3,0,0,25,108,191,244,255,255,255,255,255,255,255,255,255,255,244,191,108,25,0,0,3,0,0,0,0,0,0,1,1,0,27,140,235,255,255,255,249,248,251,254,255,255,254,251,248,249,255,255,255,235,140,27,0,1,1,0,0,0,2,0,0,110,235,255,255,249,251,255,255,255,255,246,240,240,246,255,255,255,255,251,249,255,255,235,110,0,0,2,0,1,0,25,181,255,255,249,253,255,255,216,151,92,53,29,18,18,29,53,92,151,217,255,255,253,249,255,255,181,25,0,1,
|
||||
1,30,220,255,250,251,255,255,180,74,6,0,0,0,0,0,0,0,0,0,0,6,75,180,255,255,251,250,255,220,30,1,0,124,255,244,252,255,207,69,0,0,0,6,7,4,0,0,0,0,4,7,6,0,0,0,69,207,255,252,244,255,123,0,0,76,255,255,255,147,8,0,3,6,0,0,0,0,7,15,15,7,0,0,0,0,6,3,0,8,147,255,255,255,76,0,1,0,93,151,80,0,0,5,1,0,2,67,140,193,221,235,235,221,192,138,64,0,0,1,5,0,0,80,151,93,0,1,0,0,0,0,0,2,2,0,0,97,207,255,255,255,255,255,255,255,255,255,255,204,93,0,0,2,2,0,0,0,0,0,0,0,3,4,3,2,0,37,191,255,255,251,248,255,255,255,255,255,254,248,251,255,255,187,35,0,2,3,4,3,0,0,0,0,0,0,1,1,7,212,255,248,251,255,255,246,213,193,193,214,247,255,255,250,248,255,211,7,1,1,0,0,0,0,0,0,0,0,1,0,10,235,255,255,255,197,98,31,3,0,0,3,33,100,200,255,255,255,234,10,0,1,0,0,0,0,
|
||||
0,0,0,0,0,2,0,85,216,206,95,0,0,0,1,6,6,1,0,0,0,99,208,215,81,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,4,3,0,0,0,0,3,3,0,0,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,3,0,1,0,48,119,107,25,0,1,0,3,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,2,0,95,251,255,255,230,48,0,2,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,21,241,255,249,248,255,190,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,63,255,252,255,254,253,235,14,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,46,255,251,253,251,252,225,7,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,185,255,255,255,255,124,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,0,20,163,231,223,128,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,13,8,0,0,0,0,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,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,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,0,0,0,0,0,0,0,0,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_WIFI_OFF[1024]={
|
||||
0,0,0,1,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,2,0,0,0,0,0,0,1,3,3,3,3,3,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,22,55,0,1,1,0,0,0,2,0,0,0,0,0,0,0,0,1,4,3,1,0,0,0,0,0,0,0,0,1,1,6,208,255,135,0,3,1,0,1,0,14,54,65,65,64,56,39,16,0,0,0,0,3,2,0,0,0,0,0,0,1,1,6,207,255,255,137,0,4,3,1,54,225,255,255,255,255,255,251,235,201,147,77,13,0,0,3,1,0,0,0,0,0,2,0,34,212,252,255,136,0,6,0,171,255,247,249,248,248,250,253,255,255,255,255,224,135,35,0,0,3,0,0,0,0,0,5,0,56,247,251,255,135,0,1,134,255,255,255,255,255,255,255,255,255,248,250,255,255,242,141,20,0,3,0,0,0,1,0,92,226,253,253,248,255,135,0,13,139,177,147,132,129,142,170,205,245,255,255,254,248,255,255,225,84,0,0,0,
|
||||
2,5,143,255,255,253,254,255,255,255,133,0,0,0,0,0,0,0,0,0,30,91,179,252,255,253,249,255,255,140,4,2,0,98,255,249,250,255,255,175,217,255,255,132,0,7,4,4,4,4,4,3,0,0,0,48,164,255,255,250,250,255,96,0,0,109,255,255,255,225,85,0,37,228,255,255,130,0,4,1,0,0,0,0,0,7,3,0,0,70,217,255,254,255,113,0,1,14,176,234,168,26,0,6,0,34,212,252,255,128,0,4,2,0,34,53,3,0,0,5,4,0,18,160,233,185,18,1,0,0,0,15,0,0,4,0,0,21,163,254,249,255,127,0,7,0,143,255,211,127,23,0,1,3,0,0,14,0,0,0,0,1,1,0,2,2,0,6,121,239,255,254,252,247,255,126,0,4,17,190,255,255,231,104,0,1,2,2,0,1,1,0,0,0,0,1,1,2,7,181,255,255,250,254,255,255,255,255,125,0,0,12,190,255,254,255,155,3,2,0,1,0,0,0,0,0,0,0,2,0,33,255,249,251,255,255,196,131,229,255,255,123,0,2,13,193,255,254,255,32,0,2,0,0,0,0,
|
||||
0,0,0,0,1,2,5,183,255,255,191,63,0,0,48,234,255,255,122,0,1,14,183,249,173,8,2,1,0,0,0,0,0,0,0,0,0,1,0,10,89,72,0,0,1,7,0,46,232,255,255,121,0,1,0,35,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,6,56,221,255,255,119,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,3,0,1,0,19,162,234,211,177,234,255,255,118,0,6,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,184,255,255,255,255,173,228,255,255,117,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,46,255,251,253,251,255,209,57,236,255,255,116,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,63,255,252,255,254,253,241,0,54,240,255,255,115,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,20,240,255,249,248,255,188,4,0,59,240,255,255,114,0,2,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,2,0,92,250,255,255,228,46,0,6,0,61,241,255,255,109,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,47,119,107,24,0,1,0,4,0,63,242,255,255,31,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,2,0,0,0,4,0,63,224,185,13,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,4,4,1,0,0,0,0,0,2,0,8,0,0,0,0,0,0,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,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
|
||||
};
|
||||
|
||||
static const unsigned char SB_ICON_BT_NO[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,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,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,1,1,9,153,156,6,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,3,0,61,255,255,183,8,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,3,0,66,252,247,255,186,10,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,2,3,0,65,255,253,255,255,188,11,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,44,221,195,23,0,6,0,65,255,251,227,255,255,191,13,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,85,255,255,208,23,0,1,65,255,254,69,167,255,255,193,16,2,1,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,2,5,159,255,255,209,25,0,69,255,255,63,0,175,252,255,165,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,160,255,255,212,17,55,255,255,57,16,197,251,255,150,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,159,255,255,198,88,254,254,88,198,255,255,171,6,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,158,255,255,244,254,254,244,255,255,167,2,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,158,255,255,255,255,255,255,164,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,0,153,251,253,253,251,156,1,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,18,235,254,254,236,19,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,21,204,254,253,253,253,204,20,0,3,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,3,0,20,204,255,255,255,255,255,255,203,19,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,21,205,255,255,214,254,254,214,255,255,203,19,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,21,206,255,255,151,60,255,255,60,151,255,255,203,21,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,22,206,255,255,167,0,63,255,255,66,0,169,253,255,170,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,1,21,203,255,255,163,1,1,67,255,255,54,25,206,251,255,148,0,3,0,0,0,0,0,0,0,0,0,0,0,0,4,0,92,255,255,162,0,4,0,65,255,253,101,211,255,255,176,6,2,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,22,169,140,0,1,5,0,65,255,252,252,255,255,178,5,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,65,255,253,251,255,180,5,1,2,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,1,3,3,1,0,3,0,66,252,247,255,182,6,1,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,60,255,255,183,7,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,1,8,151,157,7,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,3,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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_BT_CONN[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,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,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,1,1,8,153,155,6,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,3,0,61,255,255,181,7,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,3,0,66,252,247,255,183,8,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,1,0,2,3,0,65,255,253,255,255,185,9,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,40,219,199,25,0,6,0,65,255,251,228,255,255,187,10,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,77,255,255,210,24,0,1,65,255,254,69,170,255,255,188,13,2,1,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,2,4,153,255,255,209,25,0,69,255,255,63,0,179,252,255,156,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,155,255,255,211,15,55,255,255,57,14,197,252,255,141,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,3,5,4,0,155,255,255,195,84,254,254,84,195,255,255,165,4,3,5,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,155,255,255,241,254,254,241,255,255,163,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,96,113,15,0,4,0,155,255,255,255,255,255,255,161,0,3,1,15,113,95,2,1,0,0,0,0,0,0,0,3,0,125,255,255,183,3,3,3,0,152,251,253,253,251,154,1,2,3,3,183,255,255,125,0,3,0,0,0,0,0,0,3,0,195,254,250,247,13,0,5,0,18,235,254,254,235,18,0,5,0,13,247,250,254,195,0,3,0,0,0,0,0,0,3,0,88,250,255,140,0,5,0,20,203,253,253,253,253,202,19,0,5,0,140,255,249,87,0,3,0,0,0,
|
||||
0,0,0,0,1,0,41,54,0,2,0,19,202,255,255,255,255,255,255,200,17,0,2,0,54,41,0,1,0,0,0,0,0,0,0,0,0,1,0,0,3,0,19,203,255,255,210,254,254,210,255,255,199,16,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,2,5,0,19,202,255,255,147,58,255,255,58,148,255,255,198,18,2,4,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,19,202,255,255,165,0,64,255,255,67,0,170,253,255,161,0,4,0,0,0,0,0,0,0,0,0,0,0,0,1,2,19,198,255,255,164,1,1,67,255,255,53,28,209,251,255,140,0,3,0,0,0,0,0,0,0,0,0,0,0,0,3,0,83,255,255,165,1,4,0,65,255,253,102,213,255,255,169,5,2,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,19,166,143,1,1,5,0,65,255,252,252,255,255,172,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,65,255,253,251,255,175,3,1,2,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,1,3,3,1,0,3,0,66,252,247,255,178,5,1,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,60,255,255,180,6,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,1,1,8,151,156,6,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,3,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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_BT_DIS[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,3,1,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,0,0,0,0,1,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,1,1,8,153,155,6,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,83,165,43,0,4,0,0,0,0,3,0,61,255,255,181,7,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,1,224,255,231,47,0,4,0,0,0,3,0,66,252,247,255,182,7,0,2,0,0,0,0,0,0,0,0,0,0,0,2,0,112,255,255,232,45,0,4,0,0,3,0,65,255,253,254,255,183,7,0,2,0,0,0,0,0,0,0,0,0,0,0,2,0,122,255,255,231,44,0,4,0,3,0,65,255,251,240,255,255,183,7,0,1,0,0,0,0,0,0,0,0,0,0,0,4,0,124,255,255,230,43,0,4,3,0,64,255,253,82,188,255,255,184,10,2,1,0,0,0,0,0,0,0,
|
||||
0,0,0,1,4,0,126,255,255,229,41,0,7,0,68,254,255,59,7,187,252,255,157,0,3,0,0,0,0,0,0,0,0,0,0,0,1,4,0,127,255,255,228,40,0,2,52,255,254,60,2,183,252,255,161,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,128,255,255,227,39,0,2,108,255,70,175,255,255,188,13,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,130,255,255,226,38,0,0,116,246,255,255,188,10,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,132,255,255,225,36,0,0,121,255,187,10,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,4,0,133,255,255,223,35,0,0,85,16,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,134,248,255,222,34,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,3,1,0,220,251,255,221,33,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,2,1,5,178,254,255,252,255,220,31,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,4,176,255,255,231,254,252,255,218,30,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,3,175,255,255,179,74,254,254,255,255,217,29,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,3,174,255,255,195,6,58,255,251,212,255,255,216,28,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,7,169,255,255,194,14,0,68,255,255,52,147,253,255,214,27,0,3,0,0,0,0,0,0,0,0,0,0,0,0,3,0,74,255,255,195,14,0,1,65,255,253,83,181,253,255,255,213,26,0,3,0,0,0,0,0,0,0,0,0,0,0,1,0,26,195,178,15,0,6,0,65,255,251,241,255,255,225,255,255,212,25,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,3,0,65,255,253,253,255,183,16,144,255,255,211,24,0,2,0,0,0,0,
|
||||
0,0,0,0,0,0,0,1,1,1,1,0,3,0,66,252,247,255,183,8,0,0,149,255,255,209,23,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,61,255,255,183,8,0,3,3,0,152,255,255,201,13,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,8,151,156,7,0,2,0,1,3,0,154,255,255,31,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1,0,106,81,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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_BT_SEARCH[1024]={
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,2,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,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,2,0,67,179,74,0,4,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,3,0,191,255,251,84,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,2,0,190,252,251,252,87,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,2,0,2,0,190,255,254,255,253,91,0,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,140,229,104,0,4,3,0,189,255,230,244,255,254,95,0,3,0,0,0,2,4,1,0,0,0,0,0,0,0,0,1,0,215,255,255,110,0,6,0,187,255,175,67,250,255,255,98,0,2,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,2,1,57,238,255,255,111,0,2,188,255,190,0,61,241,253,255,52,0,4,0,75,140,10,1,1,0,0,0,0,0,0,0,2,0,59,239,255,255,115,0,188,255,188,0,98,245,255,248,44,1,4,0,206,255,116,0,4,0,0,0,0,0,0,0,0,4,0,58,239,255,255,102,177,255,177,99,255,255,248,74,0,2,4,0,134,251,210,4,1,1,0,0,0,0,0,0,0,0,4,0,57,239,255,254,243,255,242,254,255,245,70,0,0,0,0,0,49,253,252,44,0,2,0,0,0,0,0,0,0,0,0,4,0,57,238,255,255,255,255,255,243,66,0,0,53,207,52,0,11,223,255,89,0,4,0,0,0,0,0,0,0,0,0,0,4,0,59,225,253,253,252,227,64,0,4,57,233,255,113,0,0,193,255,116,0,4,0,0,0,0,0,0,0,0,0,0,0,7,0,137,255,248,255,141,0,10,0,171,255,249,126,0,0,186,255,124,0,4,0,0,0,0,0,0,0,0,0,0,4,0,107,250,252,254,252,250,109,0,4,29,210,255,108,0,1,197,255,112,0,4,0,0,
|
||||
0,0,0,0,0,0,0,4,0,105,255,255,255,255,255,255,255,107,0,0,29,177,43,0,15,230,255,79,0,3,0,0,0,0,0,0,0,0,4,0,106,255,255,235,225,255,224,234,255,255,108,0,0,0,0,0,59,255,244,28,0,2,0,0,0,0,0,0,0,4,0,107,255,255,243,54,174,255,174,52,242,255,255,107,0,5,4,0,151,252,187,0,2,0,0,0,0,0,0,0,2,0,107,255,255,242,65,0,190,255,191,0,49,240,253,255,54,0,4,0,198,255,83,0,3,0,0,0,0,0,0,3,0,101,255,255,241,62,0,2,187,255,186,0,113,246,254,248,44,0,4,0,42,83,0,1,0,0,0,0,0,0,0,1,0,224,255,239,61,0,7,0,187,255,181,118,255,255,251,77,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,93,179,55,0,4,2,0,190,255,246,255,253,251,79,0,3,0,0,0,2,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,2,0,190,255,251,252,251,80,0,4,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,2,3,2,0,0,2,0,190,252,251,252,81,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,3,0,190,255,251,83,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,2,0,64,178,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,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,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
|
||||
};
|
||||
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
static const sb_icon_t sb_icons[] = {
|
||||
{ SB_ICON_WIFI },
|
||||
{ SB_ICON_BT },
|
||||
{ SB_ICON_BATT },
|
||||
{ SB_ICON_VOL },
|
||||
{ SB_ICON_BRIGHT },
|
||||
{ SB_ICON_WIFI_STRONG },
|
||||
{ SB_ICON_WIFI_MEDIUM },
|
||||
{ SB_ICON_WIFI_WEAK },
|
||||
{ SB_ICON_WIFI_NO },
|
||||
{ SB_ICON_WIFI_OFF },
|
||||
{ SB_ICON_BT_NO },
|
||||
{ SB_ICON_BT_CONN },
|
||||
{ SB_ICON_BT_DIS },
|
||||
{ SB_ICON_BT_SEARCH },
|
||||
{ SB_ICON_BATT_CHG },
|
||||
};
|
||||
|
||||
/* 枚举索引(与 sb_icons[] 对应) */
|
||||
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 */
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* stb_image.c — stb_image 实现 TU。仅解 PNG/JPEG/BMP(其余格式关闭以减体积)。
|
||||
* self-contained:PNG 内建 zlib 解码,不依赖系统 libz/libpng。
|
||||
*/
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_NO_PSD
|
||||
#define STBI_NO_TGA
|
||||
#define STBI_NO_GIF
|
||||
#define STBI_NO_PIC
|
||||
#define STBI_NO_PNM
|
||||
#define STBI_NO_HDR
|
||||
#define STBI_FAILURE_USERMSG
|
||||
#include "stb_image.h"
|
||||
+7988
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,24 @@
|
||||
/* test_host_stub.c — 宿主逻辑测试打桩:meow_font_* / meow_ui_global_render。
|
||||
* 供 test_sb_collapse.c 链真实 meow_ui_gfx.o/meow_ui_sys.o 时提供(无 /dev/fb0、无字库的宿主环境)。
|
||||
* 仅为宿主单测编译;不进 meow_ui.a(build_sdk.sh 的 OBJS 不含本文件)。 */
|
||||
#include "meow_ui.h"
|
||||
#include "meow_font.h" /* 原型(meow_ui_gfx.c include 的同款头) */
|
||||
#include <string.h>
|
||||
|
||||
void meow_font_set_fb(unsigned w, unsigned h, unsigned pitch) { (void)w;(void)h;(void)pitch; }
|
||||
int meow_font_load(const char *p, const char *i) { (void)p;(void)i; return -1; } /* 无库:g_mfont_ready=0 → 5x7 回退 */
|
||||
void meow_font_unload(void) {}
|
||||
int meow_font_reload_tier(int t, const char *p, const char *i) { (void)t;(void)p;(void)i; return -1; }
|
||||
void meow_font_drawtext(uint16_t *fb, int x, int y, const char *s, uint16_t c, int size) { (void)fb;(void)x;(void)y;(void)s;(void)c;(void)size; }
|
||||
int meow_font_width_text(const char *s, int size) { (void)size; return (int)strlen(s) * 6; }
|
||||
int meow_font_line_height(int size) { (void)size; return 20; }
|
||||
int meow_font_config_multifont(int on, const char *gp, const char *gi,
|
||||
const meow_font_class_cfg cfg[MEOW_FCLS_COUNT],
|
||||
int *distinct, int *truncated)
|
||||
{ (void)on;(void)gp;(void)gi;(void)cfg;(void)distinct;(void)truncated; return -1; }
|
||||
|
||||
/* present() 的 App 跨 Activity 全局渲染骨架(宿主无控件,no-op) */
|
||||
void meow_ui_global_render(void) {}
|
||||
|
||||
/* meow_ui_run 触摸分流调用 meow_ui_global_hit(全局控件命中;宿主无全局控件 → 不消费) */
|
||||
int meow_ui_global_hit(int x, int y) { (void)x; (void)y; return 0; }
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,209 @@
|
||||
/* test_ime_fix.c — M2.71-IME-fix:宿主离屏验证三项修复(480x720,16bpp)。
|
||||
* ① shift 大小写正确:shift 关→小写('q'),开→大写('Q');
|
||||
* ② K_ENTER 只提交不关(meow_ime_active() 仍真),ESC 后关;
|
||||
* ③ 阻塞 IME 循环喂 on_tick(动态内容重绘)且 IME 面板仍在最上
|
||||
* (On_tick 在 IME 面板区画红色哨兵行,IME 绘制后应盖回面板色,不被 App 覆盖)。
|
||||
*
|
||||
* 编译(宿主 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 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ASCII 键盘键位(build_kb:KB_TOP=420 → 首行 y=470;GW=44,GAP=4,GCOL=48,行左缘 x=2):
|
||||
* row2 "qwertyuiop" y∈[518,554);row3 "asdfghjkl"+SHIFT y∈[566,602);
|
||||
* row5 ESC+SPACE+ENTER y∈[662,698)。 */
|
||||
#define K_Q_X 20
|
||||
#define K_Q_Y 536
|
||||
#define K_SHIFT_X 450
|
||||
#define K_SHIFT_Y 584
|
||||
#define K_ENTER_X 430
|
||||
#define K_ENTER_Y 680
|
||||
#define K_ESC_X 20
|
||||
#define K_ESC_Y 680
|
||||
|
||||
/* ---- 像素/配色辅助(离屏 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();
|
||||
assert(bb && line > 0);
|
||||
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 near(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
|
||||
|
||||
/* ---- ① 异步 shift 测试(直接 handle_tap,确定语义,无阻塞循环) ---- */
|
||||
static char g_echo[64] = "";
|
||||
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
|
||||
static void test_shift(void)
|
||||
{
|
||||
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
||||
c.mode = MEOW_IME_ASCII; c.maxlen = 32; c.on_change = ime_cb;
|
||||
meow_ui_text_input_async(&c);
|
||||
assert(meow_ime_active() == 1);
|
||||
|
||||
meow_ime_handle_tap(K_Q_X, K_Q_Y); /* shift 关 → 'q' 小写 */
|
||||
assert(strcmp(meow_ime_text(), "q") == 0);
|
||||
assert(strcmp(g_echo, "q") == 0);
|
||||
assert(meow_ime_text()[0] >= 'a' && meow_ime_text()[0] <= 'z'); /* 明确小写 */
|
||||
|
||||
meow_ime_handle_tap(K_SHIFT_X, K_SHIFT_Y); /* shift 开 */
|
||||
meow_ime_handle_tap(K_Q_X, K_Q_Y); /* 'q' → 'Q' 大写 */
|
||||
|
||||
assert(strcmp(meow_ime_text(), "qQ") == 0);
|
||||
assert(strcmp(g_echo, "qQ") == 0);
|
||||
assert(meow_ime_text()[1] >= 'A' && meow_ime_text()[1] <= 'Z'); /* 明确大写 */
|
||||
printf("PASS shift: off='q'(lower), on='Q'(upper)\n"); fflush(stdout);
|
||||
|
||||
meow_ime_handle_tap(K_ESC_X, K_ESC_Y); /* 关闭 */
|
||||
meow_ime_close(0); /* 复位(清 g_ime_done/active) */
|
||||
assert(meow_ime_active() == 0);
|
||||
}
|
||||
|
||||
/* ---- ②③ 阻塞 IME(经 meow_ui_run 驱动):K_ENTER 不关 / on_tick 喂 / IME 最上 ---- */
|
||||
static meow_widget *g_tf;
|
||||
static char g_tfbuf[32] = "";
|
||||
static int g_tick = 0;
|
||||
static int g_sentinel_painted = 0;
|
||||
static int g_test_done = 0;
|
||||
|
||||
static int g_active_after_enter = -1; /* K_ENTER 后 meow_ime_active()(期望 1) */
|
||||
static int g_active_after_esc = -1; /* ESC 后 meow_ime_active()(期望 0) */
|
||||
static int g_probe_r = -1, g_probe_g = -1, g_probe_b = -1; /* IME 面板区内取样(期望=面板色非哨兵红) */
|
||||
static char g_drv_text[64] = ""; /* shift 序列后 meow_ime_text()(期望 "qQ") */
|
||||
|
||||
/* 把 back_buf 写成 PPM(视觉目检 IME 面板渲染/最上;在 IME 空闲时调用,back_buf 稳定) */
|
||||
static void dump_ppm(const char *path)
|
||||
{
|
||||
unsigned char *bb = meow_ui_back_buf();
|
||||
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
|
||||
int W = meow_ui_width(), H = meow_ui_height();
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return;
|
||||
fprintf(f, "P6\n%d %d\n255\n", W, H);
|
||||
for (int y = 0; y < H; y++) {
|
||||
for (int x = 0; x < W; x++) {
|
||||
int r, g, b; rd_px(x, y, &r, &g, &b);
|
||||
fputc(r, f); fputc(g, f); fputc(b, f);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* App on_tick:模拟 demo 动态重绘。IME 打开时先在 IME 面板区内画一条红色哨兵行,
|
||||
* 再由 meow_ime_draw 盖回面板色 → 验证「喂 on_tick 后 IME 仍最上」的绘制顺序。 */
|
||||
static void host_tick(void)
|
||||
{
|
||||
g_tick++;
|
||||
if (meow_ime_active() && !g_sentinel_painted) {
|
||||
g_sentinel_painted = 1;
|
||||
meow_ui_fill_rect(0, 500, 480, 8, 0xFF0000); /* 哨兵行:App 内容拟 IME 面板区内 */
|
||||
meow_ui_flush_region(0, 500, 480, 8);
|
||||
}
|
||||
if (g_test_done) meow_ui_request_exit();
|
||||
}
|
||||
|
||||
/* App on_tap:命中 TextField → 打开**阻塞** IME(同 hello demo 路径) */
|
||||
static void host_tap(int x, int y)
|
||||
{
|
||||
(void)x; (void)y;
|
||||
if (g_tf && meow_widget_hit(g_tf, x, y)) meow_textfield_activate(g_tf);
|
||||
}
|
||||
|
||||
static void *driver(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */
|
||||
meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */
|
||||
usleep(160000);
|
||||
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */
|
||||
usleep(90000);
|
||||
meow_input_inject(MEOW_EV_TAP, K_SHIFT_X, K_SHIFT_Y); /* shift 开 */
|
||||
usleep(60000);
|
||||
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 开 → 'Q' → 缓冲 "qQ" */
|
||||
usleep(90000);
|
||||
strncpy(g_drv_text, meow_ime_text(), sizeof g_drv_text - 1);
|
||||
g_drv_text[sizeof g_drv_text - 1] = 0;
|
||||
|
||||
meow_input_inject(MEOW_EV_TAP, K_ENTER_X, K_ENTER_Y); /* K_ENTER:只提交不关 */
|
||||
usleep(160000);
|
||||
g_active_after_enter = meow_ime_active(); /* 期望 1(键盘仍开) */
|
||||
|
||||
rd_px(240, 500, &g_probe_r, &g_probe_g, &g_probe_b); /* IME 面板区内取样 */
|
||||
dump_ppm("/tmp/ime_panel.ppm"); /* 目检:IME 面板渲染 + 盖住哨兵 */
|
||||
printf("[drv] text=%s active_after_enter=%d probe=%02X%02X%02X sentineld=%d\n",
|
||||
g_drv_text, g_active_after_enter, g_probe_r, g_probe_g, g_probe_b,
|
||||
g_sentinel_painted); fflush(stdout);
|
||||
|
||||
meow_input_inject(MEOW_EV_TAP, K_ESC_X, K_ESC_Y); /* ESC → 关闭 */
|
||||
usleep(160000);
|
||||
g_active_after_esc = meow_ime_active(); /* 期望 0 */
|
||||
g_test_done = 1;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/ime_fix.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
const meow_theme *th = meow_theme_get();
|
||||
|
||||
test_shift(); /* ① */
|
||||
|
||||
/* ②③ 在 meow_ui_run 里驱动(g_events 仅 meow_ui_run 能设置) */
|
||||
g_tf = meow_textfield_create(10, 300, 300, 44, "Type here...", g_tfbuf, 32, MEOW_IME_ASCII);
|
||||
meow_ui_events ev; memset(&ev, 0, sizeof ev);
|
||||
ev.on_tap = host_tap;
|
||||
ev.on_tick = host_tick;
|
||||
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
|
||||
meow_ui_run(&ev);
|
||||
pthread_join(thd, NULL);
|
||||
|
||||
/* 断言 ② */
|
||||
printf("[assert] active_after_enter=%d (expect 1), active_after_esc=%d (expect 0)\n",
|
||||
g_active_after_enter, g_active_after_esc);
|
||||
assert(g_active_after_enter == 1); /* K_ENTER 不关 */
|
||||
assert(g_active_after_esc == 0); /* ESC 关 */
|
||||
|
||||
/* 断言 ①(阻塞路径经 on_change 实时同步到字段) */
|
||||
printf("[assert] blocking text=%s (expect \"qQ\"), tf=%s (expect \"qQ\")\n",
|
||||
g_drv_text, meow_textfield_text(g_tf));
|
||||
assert(strcmp(g_drv_text, "qQ") == 0);
|
||||
assert(strcmp(meow_textfield_text(g_tf), "qQ") == 0);
|
||||
|
||||
/* 断言 ③:on_tick 在 IME 打开期间被喂过(哨兵已画)+ IME 面板盖住哨兵 */
|
||||
int pr = (th->surfaceContainerHighest >> 16) & 0xFF;
|
||||
int pg = (th->surfaceContainerHighest >> 8) & 0xFF;
|
||||
int pb = (th->surfaceContainerHighest) & 0xFF;
|
||||
printf("[assert] sentinel_painted=%d probe=%02X%02X%02X panel=%02X%02X%02X\n",
|
||||
g_sentinel_painted, g_probe_r, g_probe_g, g_probe_b, pr, pg, pb);
|
||||
assert(g_sentinel_painted == 1); /* on_tick 在 IME 期间被调(重绘过) */
|
||||
assert(g_tick > 0);
|
||||
assert(near(g_probe_r, pr) && near(g_probe_g, pg) && near(g_probe_b, pb)); /* IME 面板最上 */
|
||||
/* 哨兵红 0xFF0000 与面板色显著不同,确保上面断言有判别力(面板色不应近纯红) */
|
||||
assert(!(near(pr, 0xFF) && near(pg, 0x00) && near(pb, 0x00)));
|
||||
|
||||
printf("ALL PASS\n");
|
||||
meow_ui_shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
* 两种模式(meow_config_dir 经 pthread_once 缓存,目录只在首次调用确定,故分两次跑):
|
||||
* ./test_meow_ui_config env → 设 MEOW_DATA_DIR=<mkdtemp>,验证持久目录读写
|
||||
* ./test_meow_ui_config tmp → unsetenv MEOW_DATA_DIR,验证 /tmp/meowcfg_<pid> 回退
|
||||
*
|
||||
* 任一断言失败 → 打印 FAIL 并以非零退出码结束。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.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)
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *mode = (argc > 1) ? argv[1] : "env";
|
||||
char *tmpdir = NULL;
|
||||
|
||||
if (strcmp(mode, "tmp") == 0) {
|
||||
unsetenv("MEOW_DATA_DIR");
|
||||
} else {
|
||||
tmpdir = strdup("/tmp/meowcfgtestXXXXXX");
|
||||
if (!mkdtemp(tmpdir)) { printf("FATAL: mkdtemp failed\n"); return 2; }
|
||||
setenv("MEOW_DATA_DIR", tmpdir, 1);
|
||||
}
|
||||
|
||||
printf("== meow_config test [%s] ==\n", mode);
|
||||
|
||||
const char *dir = meow_config_dir();
|
||||
CHECK(dir != NULL, "meow_config_dir() 非 NULL");
|
||||
printf(" dir = %s\n", dir ? dir : "(null)");
|
||||
if (strcmp(mode, "tmp") == 0)
|
||||
CHECK(dir && strstr(dir, "/tmp/meowcfg_") != NULL, "tmp 回退目录为 /tmp/meowcfg_<pid>");
|
||||
else
|
||||
CHECK(dir && strcmp(dir, tmpdir) == 0, "env 目录 = MEOW_DATA_DIR");
|
||||
|
||||
/* --- set/get 往返 --- */
|
||||
CHECK(meow_config_set("k1", "hello") == MEOW_CFG_OK, "set k1=hello -> MEOW_CFG_OK");
|
||||
char buf[128];
|
||||
CHECK(meow_config_get("k1", buf, sizeof buf) == MEOW_CFG_OK, "get k1 -> MEOW_CFG_OK");
|
||||
CHECK(strcmp(buf, "hello") == 0, "get k1 值 = hello");
|
||||
|
||||
/* --- 全数字 int / bool 解析 --- */
|
||||
meow_config_set("num", "42");
|
||||
CHECK(meow_config_int("num", -1) == 42, "int(num)=42");
|
||||
meow_config_set("truth", "true");
|
||||
CHECK(meow_config_bool("truth", 0) == 1, "bool(true)=1");
|
||||
meow_config_set("lie", "off");
|
||||
CHECK(meow_config_bool("lie", 1) == 0, "bool(off)=0");
|
||||
CHECK(meow_config_int("nonnum", 7) == 7, "int 非数字 -> def");
|
||||
CHECK(meow_config_bool("nonbool", 3) == 3, "bool 非法 -> def");
|
||||
|
||||
/* --- 未设键 --- */
|
||||
CHECK(meow_config_get("absent", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "get 未设键 -> MEOW_CFG_ERR_NOENT");
|
||||
CHECK(meow_config_int("absent", 9) == 9, "int 未设键 -> def");
|
||||
CHECK(meow_config_bool("absent", 1) == 1, "bool 未设键 -> def");
|
||||
|
||||
/* --- int 便捷写(经 set)再读 --- */
|
||||
char nbuf[16];
|
||||
meow_config_get("num", nbuf, sizeof nbuf);
|
||||
CHECK(strcmp(nbuf, "42") == 0, "int 存储为十进制 '42'");
|
||||
|
||||
/* --- 空值(NULL=清空为 0 字节文件,与「未设」区分)--- */
|
||||
meow_config_set("empty", "");
|
||||
CHECK(meow_config_get("empty", buf, sizeof buf) == MEOW_CFG_OK, "get 空值键 -> MEOW_CFG_OK");
|
||||
CHECK(buf[0] == '\0', "空值键读回空串");
|
||||
CHECK(meow_config_get("empty", buf, sizeof buf) != MEOW_CFG_ERR_NOENT, "空值键 ≠ 未设");
|
||||
|
||||
/* --- remove --- */
|
||||
meow_config_remove("k1");
|
||||
CHECK(meow_config_get("k1", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "remove 后 get -> NOENT");
|
||||
|
||||
/* --- key 清洗:'/' 等非法字符替换为 '_',杜绝路径穿越 --- */
|
||||
meow_config_set("a/b", "slash");
|
||||
CHECK(meow_config_get("a_b", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "slash") == 0,
|
||||
"key 'a/b' 清洗为 'a_b'");
|
||||
/* 路径穿越尝试:key 含 '/' 的非法串,不得逃出数据目录('/'→'_','.' 保留为字面量)。 */
|
||||
meow_config_set("../../etc/passwd", "evil");
|
||||
CHECK(meow_config_get(".._.._etc_passwd", buf, sizeof buf) == MEOW_CFG_OK &&
|
||||
strcmp(buf, "evil") == 0, "key '../../etc/passwd' 清洗为 '.._.._etc_passwd'('/'→'_',无穿越)");
|
||||
|
||||
/* --- 参数校验 --- */
|
||||
CHECK(meow_config_get(NULL, buf, sizeof buf) == MEOW_CFG_ERR_INVAL, "get(NULL) -> INVAL");
|
||||
CHECK(meow_config_get("x", NULL, 10) == MEOW_CFG_ERR_INVAL, "get(out=NULL) -> INVAL");
|
||||
CHECK(meow_config_get("x", buf, 0) == MEOW_CFG_ERR_INVAL, "get(outsz=0) -> INVAL");
|
||||
CHECK(meow_config_set("", "v") == MEOW_CFG_ERR_INVAL, "set(空 key) -> INVAL");
|
||||
|
||||
/* --- 多键独立 --- */
|
||||
meow_config_set("ind.a", "1");
|
||||
meow_config_set("ind.b", "2");
|
||||
meow_config_get("ind.a", buf, sizeof buf);
|
||||
CHECK(strcmp(buf, "1") == 0, "多键独立 a");
|
||||
meow_config_get("ind.b", buf, sizeof buf);
|
||||
CHECK(strcmp(buf, "2") == 0, "多键独立 b");
|
||||
meow_config_remove("ind.a");
|
||||
meow_config_get("ind.b", buf, sizeof buf);
|
||||
CHECK(strcmp(buf, "2") == 0, "删除 a 不影响 b");
|
||||
|
||||
/* --- 值截断(>MEOW_CFG_VAL_MAX 4096 截断;读合法)--- */
|
||||
{
|
||||
char big[4096 * 2];
|
||||
char rbuf[4100];
|
||||
memset(big, 'A', sizeof big - 1);
|
||||
big[sizeof big - 1] = '\0';
|
||||
meow_config_set("big", big);
|
||||
meow_config_get("big", rbuf, sizeof rbuf);
|
||||
CHECK(strlen(rbuf) == 4096, "值 >4096 截断为 4096");
|
||||
}
|
||||
|
||||
if (tmpdir) free(tmpdir);
|
||||
if (g_fail) { printf("\nRESULT: FAIL (%d)\n", g_fail); return 1; }
|
||||
printf("\nRESULT: ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,118 @@
|
||||
/* test_meow_ui_theme.c — 宿主验证 §8.1 顶级主题:MD3 × MIUIx 各 light/dark 四套配色 & 控件形态。
|
||||
*
|
||||
* 用法:build 后跑 4 次(每次一个 style×mode 组合,dump 一张 PPM):
|
||||
* for s in 0 1; do for m in 0 1; do
|
||||
* MEOW_UI_DUMP=/tmp/meow_t_${s}_${m}.ppm ./test_meow_ui_theme $s $m
|
||||
* done; done
|
||||
* s: 0=MEOW_STYLE_MD3 1=MEOW_STYLE_MIUIX; m: 0=light 1=dark
|
||||
*
|
||||
* 编译(宿主 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
|
||||
*
|
||||
* 验证要点:四套主题 primary/radius 各不相同(MD3 蓝 / MIUIx 橙 + 更大圆角),
|
||||
* 同 style 下 light/dark 的 surface 成对切换;图形用 MEOW_UI_DUMP 导出 PPM 目检。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_gfx_fill_round / meow_ui_back_buf(真实像素断言) */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
static meow_widget *g_w[16];
|
||||
static int g_wn = 0;
|
||||
|
||||
static void render_sample(const meow_theme *th)
|
||||
{
|
||||
int W = meow_ui_width(), H = meow_ui_height();
|
||||
meow_ui_clear(th->bg);
|
||||
|
||||
/* 卡片:surfaceContainerLow 底 + outlineVariant 圆角描边(radius_card) */
|
||||
meow_gfx_fill_round(20, 56, W - 40, 132, th->radius_card, th->surfaceContainerLow);
|
||||
meow_ui_draw_rect_round(20, 56, W - 40, 132, th->radius_card, th->outlineVariant);
|
||||
meow_ui_text(36, 74, "THEME SAMPLE", th->font_title, th->onSurface);
|
||||
meow_ui_text(36, 116, "surfaceContainerLow/outline", th->font_body, th->onSurfaceVariant);
|
||||
char info[64];
|
||||
snprintf(info, sizeof info, "primary=0x%06X radius=%d", th->primary, th->radius);
|
||||
meow_ui_text(36, 148, info, th->font_body, th->onSurfaceVariant);
|
||||
|
||||
/* 文字行:onSurface / onSurfaceVariant 对比 */
|
||||
meow_ui_text(20, 204, "onSurface text", th->font_body, th->onSurface);
|
||||
meow_ui_text(20, 232, "onSurfaceVariant dim", th->font_body, th->onSurfaceVariant);
|
||||
meow_ui_text(20, 260, "primary accent", th->font_body, th->primary);
|
||||
|
||||
/* 控件(真实控件走主题 radius/spacing/stroke 形态,meow_widget_draw 内取 meow_theme_get) */
|
||||
for (int i = 0; i < g_wn; i++) meow_widget_draw(g_w[i]);
|
||||
|
||||
/* 滑杆:surfaceContainer 轨道 + primary 已选段(spacing/radius 来自主题) */
|
||||
meow_gfx_fill_round(20, H - 90, W - 40, 8, 4, th->surfaceContainerHigh);
|
||||
meow_gfx_fill_round(20, H - 90, (W - 40) * 2 / 3, 8, 4, th->primary);
|
||||
|
||||
/* 开关(开):primary 圆钮(radius=控件圆角) */
|
||||
meow_gfx_fill_round(W - 90, H - 150, 52, 30, 15, th->primary);
|
||||
meow_gfx_fill_circle(W - 48, H - 135, 11, th->onPrimary);
|
||||
|
||||
/* 错误色 sample */
|
||||
meow_gfx_fill_round(20, H - 180, 120, 30, th->radius, th->errorContainer);
|
||||
meow_ui_text(36, H - 176, "error", th->font_body, th->onErrorContainer);
|
||||
|
||||
(void)H;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int style = argc > 1 ? atoi(argv[1]) : 0; /* 0=MD3 1=MIUIx */
|
||||
int dark = argc > 2 ? atoi(argv[2]) : 1; /* 0=light 1=dark */
|
||||
if (argc >= 2 && !strcmp(argv[1], "usage")) {
|
||||
fprintf(stderr, "usage: %s <style 0|1> <dark 0|1>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (!getenv("MEOW_UI_DUMP")) setenv("MEOW_UI_DUMP", "/tmp/meow_theme.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
assert(meow_ui_width() == 480 && meow_ui_height() == 720);
|
||||
|
||||
/* 控件样张(复刻 test_meow_ui_widgets 的五型按钮,验证控件形态随主题) */
|
||||
g_w[g_wn++] = meow_btn_create_variant(20, 290, 200, 52, "FILLED", MEOW_BTN_FILLED);
|
||||
g_w[g_wn++] = meow_btn_create_variant(20, 352, 200, 52, "TONAL", MEOW_BTN_TONAL);
|
||||
g_w[g_wn++] = meow_btn_create_variant(20, 414, 200, 52, "OUTLINED",MEOW_BTN_OUTLINED);
|
||||
g_w[g_wn++] = meow_btn_create_variant(20, 476, 200, 52, "TEXT", MEOW_BTN_TEXT);
|
||||
g_w[g_wn++] = meow_btn_create_variant(20, 538, 200, 52, "ELEVATED",MEOW_BTN_ELEVATED);
|
||||
|
||||
/* 切到目标 style×mode,再取主题渲染。
|
||||
* 先 set_mode(触发首次 load_settings,g_loaded 锁 1)再 set_style(设置后不再被 reload 覆盖)——
|
||||
* 宿主无 settings.conf,若先 set_style 再 set_mode,后者会 load_settings 把 g_style 重读回 MD3。 */
|
||||
meow_theme_set_mode(dark ? MEOW_THEME_DARK : MEOW_THEME_LIGHT);
|
||||
meow_ui_set_style(style);
|
||||
const meow_theme *th = meow_theme_get();
|
||||
|
||||
/* 断言:样式可感知差异(primary 不同、圆角更大、间距/描边不同)——直接约束主题包正确性 */
|
||||
if (style == MEOW_STYLE_MD3) {
|
||||
assert(th->primary == (dark ? 0x4A6A9A : 0x2A5A8A)); /* MD3 蓝 */
|
||||
assert(th->radius_card == 16 && th->radius == 12 && th->spacing == 8 && th->stroke_w == 0);
|
||||
} else {
|
||||
assert(th->primary == (dark ? 0xFF8C4A : 0xF57200)); /* MIUIx 橙 */
|
||||
assert(th->radius_card == 28 && th->radius == 20 && th->spacing == 12 && th->stroke_w == 1);
|
||||
/* 与 MD3 同明暗对比:primary 必须不同(避免"切了没感觉") */
|
||||
assert(th->primary != (dark ? 0x4A6A9A : 0x2A5A8A));
|
||||
}
|
||||
/* surface/bg 为「该 style 的派生背景」(宿主无 settings.conf → 默认面) */
|
||||
if (dark) assert(th->surface == (style == MEOW_STYLE_MIUIX ? 0x181310 : 0x141824));
|
||||
else assert(th->surface == (style == MEOW_STYLE_MIUIX ? 0xF7F3EE : 0xE8EAF0));
|
||||
|
||||
render_sample(th);
|
||||
meow_ui_flush();
|
||||
|
||||
printf("PASS theme[%d][%d] style=%s dark=%d bg=0x%06X primary=0x%06X radius=%d card=%d spacing=%d stroke=%d\n",
|
||||
style, dark, style ? "MIUIx" : "MD3", dark, th->bg, th->primary,
|
||||
th->radius, th->radius_card, th->spacing, th->stroke_w);
|
||||
printf("PASS dump: MEOW_UI_DUMP=%s\n", getenv("MEOW_UI_DUMP") ? getenv("MEOW_UI_DUMP") : "(none)");
|
||||
|
||||
meow_ui_shutdown();
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* test_meow_ui_vol.c — 宿主单元测试:SDK 状态栏音量换算纯函数 + settings.conf 读取。
|
||||
*
|
||||
* 直接编译 meow_ui_vol.c(纯函数,无 meow_ui 依赖),用临时 settings 文件验证:
|
||||
* V1 解析:launcher 落盘 "key = value"(带空格)形式能正确读到值(根因回归——
|
||||
* 旧代码朴素前缀匹配 "audio.volume=" 永远读不到 → 卡默认 60);
|
||||
* V2 解析:无空格 "key=value" / 多空格 / 头注释行 / 键缺失回退默认 / 精确匹配
|
||||
* (audio.volume 不误配 audio.volume_max);
|
||||
* V3 clamp:volume_max 上限、硬上限 100、负值兜底 0、上限异常(<0)兜底 0;
|
||||
* V4 管道(镜像 meow_ui_sys.c settings_vol):读 + clamp 到 volume_max;
|
||||
* V5 展示串:与 meow_ui_sys.c statusbar_right_str 同款 "VOL %d%%",断言显示值即
|
||||
* 当前夹后音量(含 step 语义:a.volume_step 只决定步进,展示原样不/舍入)。
|
||||
*
|
||||
* 编译(宿主 gcc,mipsel 无关):
|
||||
* gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I.
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(const char *name, int cond)
|
||||
{
|
||||
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (!cond) failures++;
|
||||
}
|
||||
|
||||
/* 写一份临时 settings 文件(lines 为已含 '\n' 的行内容;NULL 结束) */
|
||||
static const char *tmp_path;
|
||||
static void write_settings(const char **lines)
|
||||
{
|
||||
FILE *f = fopen(tmp_path, "w");
|
||||
if (!f) { perror("fopen"); exit(2); }
|
||||
for (int i = 0; lines[i]; i++) fputs(lines[i], f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* 镜像 meow_ui_sys.c 的 settings_vol():读 audio.volume + volume_max,再 clamp。 */
|
||||
static int settings_vol_of(const char *path)
|
||||
{
|
||||
int vol = meow_ui_vol_read(path, "audio.volume", MEOW_UI_VOL_DEFAULT);
|
||||
int vol_max = meow_ui_vol_read(path, "audio.volume_max", MEOW_UI_VOL_MAX_DEF);
|
||||
return meow_ui_vol_clamp(vol, vol_max);
|
||||
}
|
||||
|
||||
/* 状态栏右侧 VOL 串组装(与 meow_ui_sys.c statusbar_right_str 同款)。 */
|
||||
static void vol_display(int vol, int vol_max, char *out, size_t n)
|
||||
{
|
||||
snprintf(out, n, "VOL %d%%", meow_ui_vol_clamp(vol, vol_max));
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char base[64];
|
||||
snprintf(base, sizeof base, "/tmp/meow_vol_%d.conf", (int)getpid());
|
||||
tmp_path = base;
|
||||
|
||||
/* ---- V1 解析:launcher 落盘格式 "key = value"(带空格)——根因回归 ---- */
|
||||
{
|
||||
const char *L[] = { "# meow-settings 1\n", "audio.volume = 82\n", NULL };
|
||||
write_settings(L);
|
||||
check("V1 launcher 空格格式 audio.volume=82 -> 82",
|
||||
meow_ui_vol_read(tmp_path, "audio.volume", -1) == 82);
|
||||
}
|
||||
|
||||
/* ---- V2 解析:无空格 / 多空格 / 注释 / 缺键 / 精确匹配 ---- */
|
||||
{
|
||||
const char *L[] = { "audio.volume=40\n", NULL };
|
||||
write_settings(L);
|
||||
check("V2 无空格 audio.volume=40 -> 40",
|
||||
meow_ui_vol_read(tmp_path, "audio.volume", -1) == 40);
|
||||
|
||||
const char *L2[] = { " audio.volume = 7 \n", NULL };
|
||||
write_settings(L2);
|
||||
check("V2 多空格 audio.volume = 7 -> 7",
|
||||
meow_ui_vol_read(tmp_path, "audio.volume", -1) == 7);
|
||||
|
||||
const char *L3[] = { "display.brightness = 100\n", NULL };
|
||||
write_settings(L3);
|
||||
check("V2 键缺失 -> 默认 -1", meow_ui_vol_read(tmp_path, "audio.volume", -1) == -1);
|
||||
|
||||
const char *L4[] = { "audio.volume_max = 90\n", NULL };
|
||||
write_settings(L4);
|
||||
check("V2 audio.volume 不误配 audio.volume_max -> 默认",
|
||||
meow_ui_vol_read(tmp_path, "audio.volume", -1) == -1);
|
||||
check("V2 单独读 audio.volume_max=90 -> 90",
|
||||
meow_ui_vol_read(tmp_path, "audio.volume_max", -1) == 90);
|
||||
}
|
||||
|
||||
/* ---- V3 clamp 纯函数 ---- */
|
||||
check("V3 clamp(60,100)=60", meow_ui_vol_clamp(60, 100) == 60);
|
||||
check("V3 clamp(80,60)=60 (尊重 volume_max)", meow_ui_vol_clamp(80, 60) == 60);
|
||||
check("V3 clamp(80,100)=80", meow_ui_vol_clamp(80, 100) == 80);
|
||||
check("V3 clamp(150,100)=100 (硬上限)", meow_ui_vol_clamp(150, 100) == 100);
|
||||
check("V3 clamp(150,80)=80", meow_ui_vol_clamp(150, 80) == 80);
|
||||
check("V3 clamp(-5,100)=0 (负值兜底)", meow_ui_vol_clamp(-5, 100) == 0);
|
||||
check("V3 clamp(60,0)=0 (上限 0)", meow_ui_vol_clamp(60, 0) == 0);
|
||||
check("V3 clamp(60,-10)=0 (上限异常兜底)", meow_ui_vol_clamp(60, -10) == 0);
|
||||
check("V3 clamp(120,255)=100 (硬上限优先于大上限)", meow_ui_vol_clamp(120, 255) == 100);
|
||||
|
||||
/* ---- V4 管道:读 + clamp 到 volume_max(镜像 settings_vol)---- */
|
||||
{
|
||||
const char *L[] = { "audio.volume = 82\n", "audio.volume_max = 100\n", NULL };
|
||||
write_settings(L);
|
||||
check("V4 vol=82 max=100 -> 82", settings_vol_of(tmp_path) == 82);
|
||||
|
||||
const char *L2[] = { "audio.volume = 82\n", "audio.volume_max = 60\n", NULL };
|
||||
write_settings(L2);
|
||||
check("V4 vol=82 max=60 -> 60 (clamp)", settings_vol_of(tmp_path) == 60);
|
||||
|
||||
const char *L3[] = { "audio.volume = 150\n", "audio.volume_max = 100\n", NULL };
|
||||
write_settings(L3);
|
||||
check("V4 vol=150 max=100 -> 100 (硬上限)", settings_vol_of(tmp_path) == 100);
|
||||
|
||||
const char *L4[] = { "audio.volume = -3\n", "audio.volume_max = 100\n", NULL };
|
||||
write_settings(L4);
|
||||
check("V4 vol=-3 -> 0", settings_vol_of(tmp_path) == 0);
|
||||
|
||||
const char *L5[] = { "display.brightness = 100\n", NULL }; /* 无音量键 */
|
||||
write_settings(L5);
|
||||
check("V4 无 audio.volume -> 默认 60", settings_vol_of(tmp_path) == MEOW_UI_VOL_DEFAULT);
|
||||
}
|
||||
|
||||
/* ---- V5 展示串:VOL %d%(step 只决定步进,展示原样不/舍入)---- */
|
||||
{
|
||||
char s[16];
|
||||
vol_display(60, 100, s, sizeof s);
|
||||
check("V5 vol=60 -> \"VOL 60%\"", !strcmp(s, "VOL 60%"));
|
||||
vol_display(80, 60, s, sizeof s);
|
||||
check("V5 vol=80 max=60 -> \"VOL 60%\" (显示夹后值)", !strcmp(s, "VOL 60%"));
|
||||
vol_display(-5, 100, s, sizeof s);
|
||||
check("V5 vol=-5 -> \"VOL 0%\"", !strcmp(s, "VOL 0%"));
|
||||
vol_display(37, 100, s, sizeof s);
|
||||
check("V5 vol=37 -> \"VOL 37%\" (step 值原样展示)", !strcmp(s, "VOL 37%"));
|
||||
}
|
||||
|
||||
unlink(tmp_path);
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,447 @@
|
||||
/* test_meow_ui_widgets.c — 宿主验证 §5.8.6 Material 子集纯逻辑(create/hit/state/滚动/选中)+
|
||||
* 各组件绘制(5x7 字库兜底)dump PPM 目检。不依赖 /dev/fb0/字库(meow_font 打桩)。
|
||||
*
|
||||
* 编译(宿主 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_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。
|
||||
* 结构化列表断言用 back_buf 像素(region_has/px_near),画完即读,非目检。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
|
||||
#include "meow_ui_view.h" /* 任务② view 递归 + on_event 路由断言 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
/* ---- 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);
|
||||
}
|
||||
/* 区域内含「接近 target 色」的像素数(>0 即该色被真实绘出)。 */
|
||||
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)
|
||||
|
||||
/* 结构化列表(W_LISTVIEW)测试数据源 */
|
||||
typedef struct { const char *title; const char *sub; int ik; int right; int enabled; } lvrow_t;
|
||||
static lvrow_t g_lvrows[4] = {
|
||||
{ "Row A", "sub A", MEOW_LIST_ICON_FOLDER, MEOW_LIST_RIGHT_MORE, 1 },
|
||||
{ "Row B", "sub B", MEOW_LIST_ICON_FILE, MEOW_LIST_RIGHT_NONE, 1 },
|
||||
{ "Row C", "sub C", MEOW_LIST_ICON_NONE, MEOW_LIST_RIGHT_ARROW, 0 }, /* 禁用 */
|
||||
{ "Row D", "sub D", MEOW_LIST_ICON_FOLDER, MEOW_LIST_RIGHT_CHECK, 1 },
|
||||
};
|
||||
static int lv_src(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= 4) return 0;
|
||||
out->icon_kind = g_lvrows[idx].ik;
|
||||
out->title = g_lvrows[idx].title;
|
||||
out->subtitle = g_lvrows[idx].sub;
|
||||
out->right = g_lvrows[idx].right;
|
||||
out->on = 1;
|
||||
out->enabled = g_lvrows[idx].enabled;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 测试数据源(列表容器) */
|
||||
static int list_src(int idx, char *label, int len, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= 100) return 0;
|
||||
snprintf(label, (size_t)len, "item %d", idx);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static meow_widget *g_w[64];
|
||||
static int g_wn = 0;
|
||||
static meow_widget *W(meow_widget *w) { g_w[g_wn++] = w; return w; }
|
||||
|
||||
static void free_all(void)
|
||||
{
|
||||
for (int i = 0; i < g_wn; i++) meow_widget_free(g_w[i]);
|
||||
g_wn = 0;
|
||||
}
|
||||
|
||||
/* §6.3 通用长按测试回调(开闸长按判定用;事件本身经 meow_input_pop 断言)。 */
|
||||
static void lp_recv(int x, int y) { (void)x; (void)y; }
|
||||
|
||||
/* ---- 任务① 绘制期 clip:W_CUSTOM 越界绘制被裁掉(render 回调经 flag 切换越界)----
|
||||
* 控件几何硬编码为 (40,40,80,40)(与下方创建一致):meow_widget 是对外不透明类型,
|
||||
* 测试经 meow_widget_custom_create(40,40,80,40,...) 建在同址,回调按常量绘制。 */
|
||||
static int g_custom_oob = 1; /* 1=画越界实心块+越界文本;0=只画控件内 */
|
||||
static void clip_oob_render(meow_widget *w, const meow_theme *th, void *ctx)
|
||||
{
|
||||
(void)w; (void)th; (void)ctx;
|
||||
meow_ui_fill_rect(45, 45, 20, 20, 0xFF0000); /* 控件 (40,40,80,40) 内:必画红块 */
|
||||
if (g_custom_oob) {
|
||||
/* 越界(右/下,出控件矩形 [40..120)×[40..80)):应被 widget clip 裁掉 */
|
||||
meow_ui_fill_rect(140, 40, 60, 40, 0xFF0000);
|
||||
meow_ui_text(160, 100, "OOB", 20, 0xFF0000);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 任务② view 递归 enter/leave + on_event 子>父:trace 顺序 + 命中计数 ---- */
|
||||
static int g_vtrace[16]; static int g_vtrace_n = 0;
|
||||
static void vtrace_enter(meow_view *v, void *ctx) { (void)ctx; g_vtrace[g_vtrace_n++] = v->id; }
|
||||
static void vtrace_leave(meow_view *v, void *ctx) { (void)ctx; g_vtrace[g_vtrace_n++] = -v->id; }
|
||||
static int g_ev_child_hits = 0, g_ev_parent_hits = 0;
|
||||
static int vev_child(meow_view *v, int ev, int a, int b, void *ctx)
|
||||
{ (void)v; (void)ctx; if (ev == 7 && a == 10 && b == 10) { g_ev_child_hits++; return 1; } return 0; }
|
||||
static int vev_parent(meow_view *v, int ev, int a, int b, void *ctx)
|
||||
{ (void)v; (void)ctx; if (ev == 7) { g_ev_parent_hits++; return 1; } return 0; }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/b2.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
int xres = meow_ui_width(), yres = meow_ui_height();
|
||||
assert(xres == 480 && yres == 720);
|
||||
|
||||
/* ---------- Button 五型:create_variant/set_variant + 命中 ---------- */
|
||||
meow_widget *bf = W(meow_btn_create_variant(10, 10, 200, 56, "FILLED", MEOW_BTN_FILLED));
|
||||
meow_widget *bt = W(meow_btn_create_variant(10, 80, 200, 56, "TONAL", MEOW_BTN_TONAL));
|
||||
meow_widget *bo = W(meow_btn_create_variant(10, 150, 200, 56, "OUTLINED", MEOW_BTN_OUTLINED));
|
||||
meow_widget *bx = W(meow_btn_create_variant(10, 220, 200, 56, "TEXT", MEOW_BTN_TEXT));
|
||||
meow_widget *be = W(meow_btn_create_variant(10, 290, 200, 56, "ELEV", MEOW_BTN_ELEVATED));
|
||||
assert(meow_btn_set_variant(bt, MEOW_BTN_TONAL) == 0); /* 同值 */
|
||||
assert(meow_btn_set_variant(bo, MEOW_BTN_OUTLINED) == 0);
|
||||
assert(meow_widget_hit(bf, 100, 30) == 1);
|
||||
assert(meow_widget_hit(bf, 5, 5) == 0); /* 越界 */
|
||||
meow_widget_set_enabled(bf, 0);
|
||||
assert(meow_widget_hit(bf, 100, 30) == 0); /* 禁用不命中 */
|
||||
meow_widget_set_enabled(bf, 1);
|
||||
meow_widget_set_selected(be, 1);
|
||||
assert(meow_widget_selected(be) == 1);
|
||||
printf("PASS button-variant: create/set/hit/disabled/selected\n");
|
||||
|
||||
/* ---------- TextField ---------- */
|
||||
char tfbuf[32] = "";
|
||||
meow_widget *tf = W(meow_textfield_create(10, 360, 300, 44, "Type here...", tfbuf, 32, MEOW_IME_ASCII));
|
||||
strcpy(tfbuf, "abc");
|
||||
assert(strcmp(meow_textfield_text(tf), "abc") == 0);
|
||||
assert(meow_widget_hit(tf, 20, 380) == 1);
|
||||
meow_textfield_set_placeholder(tf, "placeholder");
|
||||
printf("PASS textfield: create/text/placeholder/hit\n");
|
||||
|
||||
/* ---------- Card + content ---------- */
|
||||
meow_widget *card = W(meow_card_create(240, 10, 220, 180));
|
||||
meow_widget *inner = W(meow_btn_create_variant(260, 40, 180, 44, "IN CARD", MEOW_BTN_FILLED));
|
||||
meow_card_set_content(card, inner);
|
||||
assert(meow_widget_hit(card, 250, 20) == 1);
|
||||
printf("PASS card: create/content/hit\n");
|
||||
|
||||
/* ---------- Divider / Badge ---------- */
|
||||
meow_widget *dv = W(meow_divider_create(10, 470, 200, 0));
|
||||
meow_widget *dv2 = W(meow_divider_create(230, 470, 8, 1));
|
||||
meow_widget *bd = W(meow_badge_create(400, 100));
|
||||
meow_badge_set(bd, 3);
|
||||
meow_badge_set(bd, 0);
|
||||
printf("PASS divider/badge: create/set\n");
|
||||
|
||||
/* ---------- Checkbox / Radio ---------- */
|
||||
meow_widget *ck = W(meow_checkbox_create(300, 200));
|
||||
meow_widget *rd = W(meow_radio_create(300, 250));
|
||||
meow_widget_set_selected(ck, 1);
|
||||
meow_widget_set_selected(rd, 1);
|
||||
assert(meow_widget_selected(ck) == 1);
|
||||
assert(meow_widget_hit(rd, 315, 265) == 1);
|
||||
printf("PASS checkbox/radio: create/select/hit\n");
|
||||
|
||||
/* ---------- ProgressBar ---------- */
|
||||
meow_widget *pg = W(meow_progress_create(10, 520, 300, 6, 40));
|
||||
meow_progress_set(pg, 200); /* clamp 100(无 getter,仅不崩) */
|
||||
meow_widget *pgc = W(meow_progress_circular_create(420, 520, 20, 66));
|
||||
meow_progress_set(pgc, -1); /* 不定 */
|
||||
printf("PASS progress: create/set linear+circular\n");
|
||||
|
||||
/* ---------- FAB ---------- */
|
||||
meow_widget *fab = W(meow_fab_create(410, 620, 56, "+"));
|
||||
assert(meow_widget_hit(fab, 430, 640) == 1);
|
||||
printf("PASS fab: create/hit\n");
|
||||
|
||||
/* ---------- TopAppBar ---------- */
|
||||
meow_widget *tab_ = W(meow_topappbar_create(0, 0, 480, 56, "Title"));
|
||||
meow_topappbar_set_back(tab_, 1);
|
||||
int a0 = meow_topappbar_add_action(tab_, "Save");
|
||||
int a1 = meow_topappbar_add_action(tab_, "Edit");
|
||||
assert(a0 == 0 && a1 == 1);
|
||||
assert(meow_topappbar_hit(tab_, 10, 28) == MEOW_TOPBAR_BACK); /* 返回区 */
|
||||
assert(meow_topappbar_hit(tab_, 440, 28) == 1); /* 右缘动作 "Edit"(最右) */
|
||||
printf("PASS topappbar: create/back/action/hit\n");
|
||||
|
||||
/* ---------- NavigationBar ---------- */
|
||||
meow_widget *nav = W(meow_navigationbar_create(0, 680, 480, 40));
|
||||
int n0 = meow_navigationbar_add_item(nav, "Home");
|
||||
int n1 = meow_navigationbar_add_item(nav, "Library");
|
||||
int n2 = meow_navigationbar_add_item(nav, "Settings");
|
||||
assert(n0 == 0 && n1 == 1 && n2 == 2);
|
||||
meow_navigationbar_set_selected(nav, 2);
|
||||
assert(meow_navigationbar_selected(nav) == 2);
|
||||
assert(meow_navigationbar_hit(nav, 420, 700) == 2); /* 第 3 项 */
|
||||
assert(meow_navigationbar_hit(nav, 10, 700) == 0);
|
||||
printf("PASS navigationbar: add/select/hit\n");
|
||||
|
||||
/* ---------- TabBar ---------- */
|
||||
meow_widget *tbb = W(meow_tabbar_create(0, 660, 480, 36));
|
||||
meow_tabbar_add_tab(tbb, "Playlist");
|
||||
meow_tabbar_add_tab(tbb, "Queue");
|
||||
meow_tabbar_set_selected(tbb, 1);
|
||||
assert(meow_tabbar_hit(tbb, 300, 678) == 1);
|
||||
printf("PASS tabbar: add/select/hit\n");
|
||||
|
||||
/* ---------- 列表容器(LazyColumn 虚拟化 + 滚动) ---------- */
|
||||
meow_widget *lc = W(meow_lazycolumn_create(20, 460, 260, 180, 40, 100, list_src, NULL));
|
||||
assert(meow_list_scroll(lc) == 0);
|
||||
assert(meow_list_max_scroll(lc) == 100 * 40 - 180); /* 4000-180=3820 */
|
||||
meow_list_set_scroll(lc, 5000);
|
||||
assert(meow_list_scroll(lc) == 3820); /* clamp 到 max */
|
||||
meow_list_scroll_by(lc, -320);
|
||||
assert(meow_list_scroll(lc) == 3500);
|
||||
assert(meow_list_hit_index(lc, 30, 480) == ((480 - 460 + 3500) / 40)); /* 可见项换算 */
|
||||
meow_list_set_item_h(lc, 50);
|
||||
assert(meow_widget_hit(lc, 30, 470) == 1);
|
||||
/* M2.71-bug8:行选中 + 命中点击回调 */
|
||||
meow_list_set_selected(lc, 5);
|
||||
assert(meow_list_selected(lc) == 5);
|
||||
/* 验证 tap_at 返回命中下标并置选中 */
|
||||
int hidx = meow_list_tap_at(lc, 30, 480);
|
||||
assert(hidx == ((480 - 460 + meow_list_scroll(lc)) / 50));
|
||||
assert(meow_list_selected(lc) == hidx);
|
||||
/* LazyRow 横向 */
|
||||
meow_widget *lr = W(meow_lazyrow_create(20, 640, 380, 40, 60, 100, list_src, NULL));
|
||||
meow_list_scroll_by(lr, 200);
|
||||
assert(meow_list_hit_index(lr, 40, 660) == ((40 - 20 + 200) / 60));
|
||||
printf("PASS lazy list: scroll clamp/virtualize/hit_index/tap_at/select (col+row)\n");
|
||||
|
||||
/* ---------- 结构化列表(W_LISTVIEW):行渲染像素 + 命中 + 滚动裁剪 ----------
|
||||
* 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));
|
||||
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;
|
||||
int textx = lvx + 16 + 24 + 16; /* 文本起 x = 76 */
|
||||
int op_start = lvx + (lvw * 2 / 3); /* 行右 1/3 操作区起 = 20+213=233 */
|
||||
int sb_r, sb_g, sb_b, os_r, os_g, os_b, pc_r, pc_g, pc_b;
|
||||
int ospv_r, ospv_g, ospv_b, prim_r, prim_g, prim_b;
|
||||
UNPK(lth->bg, sb_r, sb_g, sb_b);
|
||||
UNPK(lth->onSurface, os_r, os_g, os_b);
|
||||
UNPK(lth->primaryContainer, pc_r, pc_g, pc_b);
|
||||
UNPK(lth->onSurfaceVariant, ospv_r, ospv_g, ospv_b);
|
||||
UNPK(lth->primary, prim_r, prim_g, prim_b);
|
||||
int thh = meow_ui_text_height(lth->font_body); /* 20(宿主 5x7) */
|
||||
|
||||
/* 行 0 的标题/副标题 y(含 subtitle 的两行居中):ty=240+(56-42)/2=247 */
|
||||
int r0_ty = lvy + (itemh - (2 * thh + 2)) / 2;
|
||||
int r0_sy = r0_ty + thh + 2;
|
||||
|
||||
/* 1) 行内容渲染:清底 + 只画列表,断言图标/标题/副标题/右侧 ⋯ 真实像素 */
|
||||
meow_ui_clear(lth->bg);
|
||||
meow_widget_draw(lv);
|
||||
assert(region_has(lvx + 16, lvy + (itemh - 24) / 2, 24, 24, prim_r, prim_g, prim_b) > 100);
|
||||
assert(region_has(textx, r0_ty, 80, thh, os_r, os_g, os_b) > 2);
|
||||
assert(region_has(textx, r0_sy, 80, thh, ospv_r, ospv_g, ospv_b) > 2);
|
||||
assert(region_has(lvx + lvw - 16 - 24, lvy + (itemh - 20) / 2, 8, 20, ospv_r, ospv_g, ospv_b) > 20);
|
||||
printf("PASS listview: row render (icon/title/subtitle/right-more) pixels\n");
|
||||
|
||||
/* 2) 选中态:行 1 → primaryContainer 高亮;行 0 无高亮(bg) */
|
||||
meow_listview_set_sel(lv, 1);
|
||||
meow_ui_clear(lth->bg);
|
||||
meow_widget_draw(lv);
|
||||
assert(px_near(28, lvy + itemh + 48, pc_r, pc_g, pc_b)); /* 行1带底部空白 = primaryContainer */
|
||||
assert(px_near(28, lvy + 48, sb_r, sb_g, sb_b)); /* 行0带空白 = bg(未选中无高亮) */
|
||||
int selout[4];
|
||||
assert(meow_listview_get_sel(lv, selout, 4) == 1 && selout[0] == 1);
|
||||
assert(meow_listview_sel_count(lv) == 1);
|
||||
printf("PASS listview: selected row highlight + get_sel/sel_count\n");
|
||||
|
||||
/* 3) 命中:行号 + 右侧操作区分 + 禁用行不命中 */
|
||||
assert(meow_listview_hit(lv, 28, lvy + 20) == 0); /* 行0 体 */
|
||||
assert(meow_listview_hit_op(lv, 28, lvy + 20) == 0); /* 体非操作区 */
|
||||
assert(meow_listview_hit_op(lv, op_start + 10, lvy + 20) == 1); /* 行0 右1/3=操作区 */
|
||||
meow_listview_set_sel(lv, -1);
|
||||
/* 滚动到 3、4 行行内命中(scroll=112 → 行2 y240、行3 y296) */
|
||||
meow_listview_set_scroll(lv, 112);
|
||||
assert(meow_listview_scroll(lv) == 112);
|
||||
assert(meow_listview_max_scroll(lv) == 4 * 56 - 112);
|
||||
assert(meow_listview_hit(lv, 28, lvy + 28) == -1); /* 行2 禁用 → 不命中 */
|
||||
assert(meow_listview_hit(lv, 28, lvy + itemh + 28) == 3); /* 行3 命中 */
|
||||
printf("PASS listview: hit row/hit_op/disabled-not-hit\n");
|
||||
|
||||
/* 4) 滚动视口裁剪:滚到 112 后画,视口外(上缘/下缘)无泄漏(保持 bg) */
|
||||
meow_ui_clear(lth->bg);
|
||||
meow_widget_draw(lv);
|
||||
assert(px_near(21, lvy - 1, sb_r, sb_g, sb_b)); /* 视口上缘外 = bg(无上漏) */
|
||||
assert(px_near(21, lvy + lvh, sb_r, sb_g, sb_b)); /* 视口下缘外 = bg(无下漏) */
|
||||
/* 视口内 行3 标题真实渲染(行3 y=296,标题 y=303) */
|
||||
assert(region_has(textx, lvy + itemh + (itemh - (2 * thh + 2)) / 2,
|
||||
80, thh, os_r, os_g, os_b) > 2);
|
||||
/* 行 2 禁用行「变淡」:draw 后其左 padding 像素应被 dim 覆盖(偏离纯 bg) */
|
||||
assert(!px_near(28, lvy + 28, sb_r, sb_g, sb_b));
|
||||
printf("PASS listview: scroll clip no-leak + disabled-row dim\n");
|
||||
meow_listview_set_scroll(lv, 0);
|
||||
meow_listview_set_sel(lv, -1);
|
||||
|
||||
/* ---------- Dialog / BottomSheet 纯逻辑(create/set/add;show 阻塞故不调) ---------- */
|
||||
meow_dialog *dlg = meow_dialog_create(MEOW_DLG_SELECT, "Pick one");
|
||||
const char *items[] = { "Alpha", "Beta", "Gamma" };
|
||||
meow_dialog_set_items(dlg, items, 3, 1);
|
||||
assert(meow_dialog_result(dlg) == 0); /* 未 show 默认 */
|
||||
meow_dialog_free(dlg);
|
||||
meow_dialog *dc = meow_dialog_create(MEOW_DLG_CONFIRM, "Sure?");
|
||||
assert(meow_dialog_result(dc) == 0);
|
||||
meow_dialog_free(dc);
|
||||
meow_sheet *sh = meow_sheet_create("Options", 0);
|
||||
int s0 = meow_sheet_add_item(sh, "Copy");
|
||||
int s1 = meow_sheet_add_item(sh, "Delete");
|
||||
assert(s0 == 0 && s1 == 1);
|
||||
meow_sheet_free(sh);
|
||||
printf("PASS dialog/sheet: create/set_items/add_item (blocking show skipped)\n");
|
||||
|
||||
/* ---------- §6.3 通用长按:触发/取消/短按不受影响(模拟时序) ----------
|
||||
* 用 meow_input_touch_set(按下+已按住时长回拨)驱动 SDK 触摸状态机,meow_input_long_press_poll
|
||||
* 检测阈值跨越;meow_input_touch_release 走真实 touch_release(含长按后抑制 TAP/SWIPE)。
|
||||
* 断言:① 长按触发(poll=1 + 入队 LONG_PRESS);② 长按后抬手不再产生 TAP;③ 短按不受影响;
|
||||
* ④ 移动超容忍取消长按;⑤ 阈值可配;⑥ 未注册回调 = 长按关闭(opt-in,hello 等零回归)。 */
|
||||
{
|
||||
meow_input_ev e;
|
||||
meow_input_drain();
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 复位(清任何残留按住态) */
|
||||
meow_ui_set_long_press_ms(500); /* 显式回默认,防其它段干扰 */
|
||||
|
||||
/* ⑥ opt-in:未注册回调 → 长按判定关闭:poll=0(即使 hold≥阈值),抬手照常 TAP。 */
|
||||
assert(meow_input_long_press_poll() == 0);
|
||||
meow_input_touch_set(1, 130, 310, 700);
|
||||
assert(meow_input_long_press_poll() == 0); /* 未注册回调:不触发 */
|
||||
meow_input_touch_release();
|
||||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_TAP);
|
||||
meow_ui_set_long_press_cb(lp_recv);
|
||||
|
||||
/* ① 长按触发:hold=700ms(≥500)→ poll=1,LONG_PRESS 入队且坐标=按下点。 */
|
||||
meow_input_touch_set(1, 120, 300, 700);
|
||||
assert(meow_input_long_press_poll() == 1);
|
||||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_LONG_PRESS && e.a == 120 && e.b == 300);
|
||||
|
||||
/* ② 长按后抬手:点击被抑制(不再产生 TAP)。 */
|
||||
meow_input_touch_release();
|
||||
assert(meow_input_pop(&e) == 0);
|
||||
|
||||
/* ③ 短按不受影响:hold=100ms(<500)→ poll=0(不触发);抬手 → TAP 正常。 */
|
||||
meow_input_touch_set(1, 200, 400, 100);
|
||||
assert(meow_input_long_press_poll() == 0);
|
||||
meow_input_touch_release();
|
||||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_TAP && e.a == 200 && e.b == 400);
|
||||
|
||||
/* ④ 长按取消(移动超容忍):hold=700ms 但按下后位移 30px(>12)→ poll=0;抬手仍 TAP
|
||||
* (位移 < SWIPE_THRESH 65,手势判定为点击)。 */
|
||||
meow_input_touch_set(1, 300, 500, 700);
|
||||
meow_input_touch_move(300 + 30, 500);
|
||||
assert(meow_input_long_press_poll() == 0);
|
||||
meow_input_touch_release();
|
||||
assert(meow_input_pop(&e) == 1 && e.type == MEOW_EV_TAP);
|
||||
|
||||
/* ⑤ 阈值可配:设 200ms → hold=250ms 也触发;恢复默认。 */
|
||||
meow_ui_set_long_press_ms(200);
|
||||
assert(meow_ui_long_press_ms() == 200);
|
||||
meow_input_touch_set(1, 180, 260, 250);
|
||||
assert(meow_input_long_press_poll() == 1);
|
||||
meow_input_touch_release();
|
||||
meow_ui_set_long_press_ms(500);
|
||||
assert(meow_ui_long_press_ms() == 500);
|
||||
|
||||
meow_ui_set_long_press_cb(NULL); /* 复位,避免影响其它段 */
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 复位,避免影响后续绘制 */
|
||||
meow_input_drain();
|
||||
printf("PASS long-press: trigger/suppress-tap/short-tap/cancel-move/configurable/opt-in\n");
|
||||
}
|
||||
|
||||
/* ---------- 任务① 绘制期 clip:W_CUSTOM 越界绘制被裁掉 ---------- */
|
||||
{
|
||||
const meow_theme *thc = meow_theme_get();
|
||||
int bg_r, bg_g, bg_b; UNPK(thc->bg, bg_r, bg_g, bg_b);
|
||||
meow_widget *cw = W(meow_widget_custom_create(40, 40, 80, 40, clip_oob_render, NULL, NULL));
|
||||
/* a) clip 生效:经 meow_widget_draw(push 控件矩形)→ 越界块/文本被裁掉 */
|
||||
g_custom_oob = 1;
|
||||
meow_ui_clear(thc->bg);
|
||||
meow_widget_draw(cw);
|
||||
assert(region_has(45, 45, 20, 20, 0xFF, 0x00, 0x00) > 30); /* 控件内红块画出 */
|
||||
assert(px_near(40 + 80 + 25, 40 + 10, bg_r, bg_g, bg_b)); /* 越界实心块被裁 → bg */
|
||||
assert(px_near(40 + 80 + 45, 40 + 65, bg_r, bg_g, bg_b)); /* 越界文本被裁 → bg */
|
||||
/* b) 对照:无 clip 直绘同尺寸块会画出(证明是 clip 在裁) */
|
||||
meow_ui_clear(thc->bg);
|
||||
meow_ui_fill_rect(40 + 80 + 20, 40, 60, 40, 0x00FF00);
|
||||
assert(region_has(40 + 80 + 20, 40, 60, 40, 0x00, 0xFF, 0x00) > 100);
|
||||
printf("PASS clip: W_CUSTOM out-of-bounds fill+text clipped by widget clip\n");
|
||||
}
|
||||
|
||||
/* ---------- 任务② view 递归 enter/leave + on_event 子>父命中 ---------- */
|
||||
{
|
||||
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 };
|
||||
/* 页 id 须 < MEOW_VIEW_BY_ID_MAX(32) 才能进 g_view_by_id(page_push 查找) */
|
||||
meow_view *pv = meow_view_create(3, MEOW_VIEW_ACTIVITY, &ph, NULL);
|
||||
meow_view *cv = meow_view_create(4, MEOW_VIEW_CONTAINER, &ch, NULL);
|
||||
assert(meow_view_add_child(pv, cv) == 0);
|
||||
g_vtrace_n = 0; g_ev_child_hits = 0; g_ev_parent_hits = 0;
|
||||
meow_view_page_push(3); /* 进入 pv → 递归进 cv:pv.on_enter→cv.on_enter */
|
||||
assert(g_vtrace_n == 2 && g_vtrace[0] == 3 && g_vtrace[1] == 4);
|
||||
/* 命中 (10,10):先发子(按 z 降序),子命中即消费,父不再收 */
|
||||
assert(meow_view_dispatch_event(7, 10, 10) == 1);
|
||||
assert(g_ev_child_hits == 1 && g_ev_parent_hits == 0);
|
||||
/* 命中 (20,20):子不命中 → 落父 on_event */
|
||||
assert(meow_view_dispatch_event(7, 20, 20) == 1);
|
||||
assert(g_ev_child_hits == 1 && g_ev_parent_hits == 1);
|
||||
/* 离开 pv → 递归离开:cv.on_leave(-4) → pv.on_leave(-3) */
|
||||
g_vtrace_n = 0;
|
||||
meow_view_page_pop();
|
||||
assert(g_vtrace_n == 2 && g_vtrace[0] == -4 && g_vtrace[1] == -3);
|
||||
meow_view_free(pv); /* 递归释放 cv */
|
||||
printf("PASS view: recursive enter/leave order + on_event child>parent hit\n");
|
||||
}
|
||||
|
||||
/* ---------- 绘制所有控件到 back_buf(5x7 兜底),flush 出屏幕 ---------- */
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
for (int i = 0; i < g_wn; i++) meow_widget_draw(g_w[i]);
|
||||
meow_ui_flush();
|
||||
printf("PASS draw: all %d widgets rendered, screen dumps to /tmp/b2.ppm\n", g_wn);
|
||||
|
||||
free_all();
|
||||
meow_ui_shutdown();
|
||||
printf("ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,120 @@
|
||||
/* test_notif_gesture.c — M2.87 宿主验证「顶部下滑 → 通知中心」手势优先分类(meow_ui_input.c):
|
||||
* 复现并断言修复:起点在顶部热区 + 有真实向下位移(mdy>0 且 ady≥SWIPE_THRESH)的下滑,必须
|
||||
* 分类为 MEOW_EV_NOTIF_PULL(系统保留手势,路由 meow_ui_sys_notif_pull → MSG_NOTIF_OPEN),
|
||||
* 即便该手势同时落在左缘且向右位移 ≥ ady(旧代码被水平分量抢判为 MEOW_SWIPE_LEFT → 漏给
|
||||
* App on_swipe → 根页退出)。纯水平右滑/起点不在热区的下滑不误判。
|
||||
*
|
||||
* 用例(经 meow_input_touch_set/move/release 驱动真实 touch_release + 队列,pop 断言):
|
||||
* T1 顶部+左缘+向下右侧滑 (10,5)->(110,75):adx=100≥ady=70, 热区内, ady≥65 → NOTIF_PULL(旧代码→LEFT)
|
||||
* T2 顶部+左缘 竖直下滑 (240,5)->(240,120):ady=115, 热区内 → NOTIF_PULL
|
||||
* T3 顶部+左缘 下移<65 (10,5)->(100,40):ady=35<65 → 非 NOTIF_PULL → LEFT(水平占优)
|
||||
* T4 顶部+左缘 纯水平右滑 (5,5)->(110,5):ady=0<65 → 非 NOTIF_PULL → LEFT
|
||||
* T5 中部(非热区)下滑 (240,100)->(240,220):ady=120 但原点 y=100≥热区 → SWIPE DOWN(内容滚动)
|
||||
* T6 左缘中部 右滑返回 (5,300)->(110,300):非热区, 水平占优 → SWIPE LEFT(返回)
|
||||
* T7 顶部中央 下滑后回弹 (240,5)->(240,-... 不适用;改用 (240,5)->(240,90) 大 ady) → NOTIF_PULL
|
||||
*
|
||||
* 编译(宿主 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
|
||||
* 运行:./test_notif_gesture
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <assert.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* MEOW_EV_NOTIF_PULL 是 meow_ui_input.c 内部常量(未入公共头),宿主测试按同值声明。 */
|
||||
#ifndef MEOW_EV_NOTIF_PULL
|
||||
#define MEOW_EV_NOTIF_PULL 5
|
||||
#endif
|
||||
|
||||
/* 与 meow_ui_input.c 同阈值(防漂移:直接引用会打破封装,这里显式对齐注释值)。 */
|
||||
#define SWIPE_THRESH 65
|
||||
#define EDGE_W 24
|
||||
#define HOTZONE 28 /* meow_ui_statusbar_hotzone():非全屏=状态栏高 MUI_SBAR_H */
|
||||
|
||||
static int g_fail = 0;
|
||||
|
||||
/* 驱动一次手势并返回队列中首个(应唯一)语义事件类型;无事件返回 -1。
|
||||
* 顺序:drain → touch_set(按下) → touch_move(终点) → touch_release(抬手) → pop。 */
|
||||
static int gesture_ev(int x0, int y0, int x1, int y1, int *a, int *b)
|
||||
{
|
||||
meow_input_drain();
|
||||
meow_input_touch_set(1, x0, y0, 0); /* 按下锁定原点 (x0,y0) */
|
||||
meow_input_touch_move(x1, y1); /* 移动到终点 */
|
||||
meow_input_touch_release(); /* 抬手:touch_release 做手势分类 */
|
||||
meow_input_touch_set(0, 0, 0, 0); /* 复位触摸状态机 */
|
||||
meow_input_ev e;
|
||||
if (!meow_input_pop(&e)) { *a = *b = -1; return -1; }
|
||||
*a = e.a; *b = e.b;
|
||||
return e.type;
|
||||
}
|
||||
|
||||
static void expect(const char *name, int got, int want, int a, int b)
|
||||
{
|
||||
if (got == want) {
|
||||
printf("PASS %s: type=%d (a=%d b=%d)\n", name, got, a, b);
|
||||
} else {
|
||||
printf("FAIL %s: got type=%d want=%d (a=%d b=%d)\n", name, got, want, a, b);
|
||||
g_fail = 1;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
/* 默认非全屏 / 状态栏激活 → 热区 28 */
|
||||
assert(meow_ui_statusbar_hotzone() == HOTZONE);
|
||||
|
||||
int a = 0, b = 0, t = 0;
|
||||
|
||||
/* T1 顶部+左缘 向下右侧滑:旧代码 → SWIPE_LEFT;修复后 → NOTIF_PULL(顶部优先)。
|
||||
* adx=100 ≥ ady=70,且 t_dx=10<EDGE_W、右移≥65——正是真机「状态栏下拉被当左缘返回」场景。 */
|
||||
t = gesture_ev(10, 5, 110, 75, &a, &b);
|
||||
expect("T1 top-left down-right -> NOTIF_PULL", t, MEOW_EV_NOTIF_PULL, a, b);
|
||||
assert(a == 5); /* e.a = 按下原点 Y(热区判定复核用) */
|
||||
|
||||
/* T2 顶部中央 竖直下滑(纯纵向):NOTIF_PULL */
|
||||
t = gesture_ev(240, 5, 240, 120, &a, &b);
|
||||
expect("T2 top-center vertical down -> NOTIF_PULL", t, MEOW_EV_NOTIF_PULL, a, b);
|
||||
|
||||
/* T3 顶部+左缘 下移 <SWIPE_THRESH(40px):非真实下拉 → 仍走水平占优 → LEFT(不误判 DOWN) */
|
||||
t = gesture_ev(10, 5, 100, 40, &a, &b);
|
||||
expect("T3 top-left small-down -> LEFT", t, MEOW_EV_SWIPE, a, b);
|
||||
assert(a == MEOW_SWIPE_LEFT);
|
||||
|
||||
/* T4 顶部+左缘 纯水平右滑:ady=0 → 非下拉 → LEFT */
|
||||
t = gesture_ev(5, 5, 110, 5, &a, &b);
|
||||
expect("T4 top-left pure right -> LEFT", t, MEOW_EV_SWIPE, a, b);
|
||||
assert(a == MEOW_SWIPE_LEFT);
|
||||
|
||||
/* T5 中部(原点 y=100 ≥ 热区 28)大下滑:非 NOTIF_PULL → SWIPE DOWN(内容滚动不误判) */
|
||||
t = gesture_ev(240, 100, 240, 220, &a, &b);
|
||||
expect("T5 mid down (origin below hotzone) -> SWIPE DOWN", t, MEOW_EV_SWIPE, a, b);
|
||||
assert(a == MEOW_SWIPE_DOWN);
|
||||
|
||||
/* T6 左缘中部 右滑返回(非热区):SWIPE LEFT */
|
||||
t = gesture_ev(5, 300, 110, 300, &a, &b);
|
||||
expect("T6 left-edge mid right -> SWIPE LEFT", t, MEOW_EV_SWIPE, a, b);
|
||||
assert(a == MEOW_SWIPE_LEFT);
|
||||
|
||||
/* T7 顶部中央 大幅竖直下滑:NOTIF_PULL */
|
||||
t = gesture_ev(240, 5, 240, 90, &a, &b);
|
||||
expect("T7 top-center vertical down (large) -> NOTIF_PULL", t, MEOW_EV_NOTIF_PULL, a, b);
|
||||
|
||||
/* T8 SIGUSR1 护栏:meow_ui_init 已装无害处理器。raise(SIGUSR1)(等价 watchdog/测试路径
|
||||
* 泄漏进来)——若无护栏,默认动作会终止本进程(return 走不到下一行);护栏记日志+忽略 → 存活。 */
|
||||
raise(SIGUSR1);
|
||||
printf("PASS T8 sigusr1-guard: survived raise(SIGUSR1)\n");
|
||||
|
||||
meow_ui_shutdown();
|
||||
if (g_fail) { printf("RESULT: FAIL\n"); return 1; }
|
||||
printf("RESULT: ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,191 @@
|
||||
/* test_notif_open.c — 宿主验证 §10.5「App 前台划出通知中心」的 SDK 侧上报链路:
|
||||
* 1. m_notif_open_t / m_notif_close_t 帧组装解析(round-trip)。
|
||||
* 2. meow_notify_ipc_notif_open/close 经真实 AF_UNIX socket 发到 mock launcher,
|
||||
* 服务端收到 MSG_NOTIF_OPEN / MSG_NOTIF_CLOSE 且字段正确。
|
||||
* 3. meow_ui_sys_notif_pull(origin_y)(顶部下滑系统手势钩子)→ 经 IPC 发 MSG_NOTIF_OPEN,
|
||||
* 服务端收到(origin_y/hotzone 由 SDK 填)。不渲染系统 UI——纯上报。
|
||||
* 4. meow_notify_ipc_send_raw 的 want_ack 路径(服务端回 ACK,不阻塞)。
|
||||
*
|
||||
* 编译(宿主 gcc,链 meow_notify_ipc + meow_ui_sys + gfx/theme/vol/log + 打桩 font/global):
|
||||
* 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
|
||||
* 运行:./test_notif_open
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_sys.h" /* meow_ui_sys_notif_pull */
|
||||
#include "meow_notify_ipc.h" /* meow_notify_ipc_notif_open/close + send_raw */
|
||||
#include "meow_ipc.h" /* MSG_NOTIF_OPEN/CLOSE + m_notif_open_t/m_notif_close_t */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#define TEST_SOCK "/tmp/meow_notif_open_test.sock"
|
||||
#define ACK_CODE 0x4D00u /* 普通测试 ACK 载荷标记 */
|
||||
|
||||
static int g_srv_fd = -1;
|
||||
static int g_stop = 0;
|
||||
|
||||
/* 服务端捕获到的帧(互斥保护) */
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static struct {
|
||||
uint16_t type; /* 收到的帧 type */
|
||||
uint16_t plen; /* payload 长 */
|
||||
uint8_t pay[256];
|
||||
} g_rx[8];
|
||||
static int g_rx_n = 0;
|
||||
|
||||
static int srv_send(int fd, uint16_t type, uint8_t *pay, uint32_t len)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION;
|
||||
uint32_t ack = ACK_CODE;
|
||||
memcpy(hdr+0,&magic,4); memcpy(hdr+4,&ver,2); memcpy(hdr+6,&type,2); memcpy(hdr+8,&ack,4); memcpy(hdr+12,&len,4);
|
||||
size_t off = 0;
|
||||
while (off < sizeof hdr) { ssize_t w = write(fd, hdr+off, sizeof hdr-off); if (w<=0) return -1; off+=(size_t)w; }
|
||||
if (len && pay) { off=0; while (off<len){ ssize_t w=write(fd, pay+off, len-off); if(w<=0)return -1; off+=(size_t)w; } }
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *srv_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (lfd < 0) return NULL;
|
||||
unlink(TEST_SOCK);
|
||||
struct sockaddr_un a; memset(&a,0,sizeof a); a.sun_family=AF_UNIX; strncpy(a.sun_path,TEST_SOCK,sizeof a.sun_path-1);
|
||||
if (bind(lfd,(struct sockaddr*)&a,sizeof a)<0){ perror("bind"); close(lfd); return NULL; }
|
||||
chmod(TEST_SOCK,0666);
|
||||
if (listen(lfd,4)<0){ perror("listen"); close(lfd); return NULL; }
|
||||
fcntl(lfd, F_SETFL, fcntl(lfd,F_GETFL,0)|O_NONBLOCK); /* 非阻塞 accept:g_stop 时及时退出 */
|
||||
g_srv_fd = lfd;
|
||||
while (!g_stop) {
|
||||
int c = accept(lfd, NULL, NULL);
|
||||
if (c < 0) {
|
||||
if (errno==EAGAIN || errno==EWOULDBLOCK) { usleep(20000); continue; }
|
||||
if (errno==EINTR) continue;
|
||||
break;
|
||||
}
|
||||
/* 读帧(一连接收若干帧;本测试每连接一帧) */
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
size_t off=0;
|
||||
while (off<sizeof hdr){ ssize_t r=read(c,hdr+off,sizeof hdr-off); if(r<=0) goto next; off+=(size_t)r; }
|
||||
uint32_t magic,len; uint16_t ver,type;
|
||||
memcpy(&magic,hdr,4); memcpy(&ver,hdr+4,2); memcpy(&type,hdr+6,2); memcpy(&len,hdr+12,4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION || len > 256) goto next;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_rx_n < 8) {
|
||||
uint8_t pay[256]; memset(pay,0,sizeof pay);
|
||||
off=0; while(off<len){ ssize_t r=read(c,pay+off,len-off); if(r<=0)break; off+=(size_t)r; }
|
||||
g_rx[g_rx_n].type=type; g_rx[g_rx_n].plen=(uint16_t)len; memcpy(g_rx[g_rx_n].pay,pay,len);
|
||||
g_rx_n++;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
/* 回 ACK(m_ack_t,code=MEOW_OK, status=1);best-effort */
|
||||
{
|
||||
m_ack_t a; a.code=MEOW_OK; a.status=1;
|
||||
(void)srv_send(c, MSG_ACK, (uint8_t*)&a, sizeof a);
|
||||
}
|
||||
next:
|
||||
close(c);
|
||||
}
|
||||
close(lfd);
|
||||
unlink(TEST_SOCK);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 取第 n 帧(n 从 0 起);返回 1=有,0=无。 */
|
||||
static int rx_get(int n, uint16_t *type, uint16_t *plen, uint8_t *pay, int cap)
|
||||
{
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int ok = (n < g_rx_n);
|
||||
if (ok) { *type = g_rx[n].type; *plen = g_rx[n].plen;
|
||||
size_t c = g_rx[n].plen < (uint16_t)cap ? g_rx[n].plen : (uint16_t)cap;
|
||||
memcpy(pay, g_rx[n].pay, c); }
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_IPC_SOCK", TEST_SOCK, 1);
|
||||
setenv("MEOW_LOG_APP", "os.meow.hello", 1);
|
||||
|
||||
pthread_t th; pthread_create(&th,NULL,srv_thread,NULL);
|
||||
/* 等服务端就绪 */
|
||||
for (int i=0;i<50 && g_srv_fd<0;i++) usleep(20000);
|
||||
if (g_srv_fd < 0) { fprintf(stderr,"server not ready\n"); return 1; }
|
||||
|
||||
int fail = 0;
|
||||
|
||||
/* 1) meow_notify_ipc_notif_open 发 MSG_NOTIF_OPEN(want_ack=1 → 服务端回 ACK,不阻塞) */
|
||||
meow_notify_ipc_notif_open("os.meow.hello", 7, 3, 8, 28, 0);
|
||||
usleep(100000);
|
||||
uint16_t t, pl; uint8_t pay[256];
|
||||
if (rx_get(0,&t,&pl,pay,sizeof pay) && t==MSG_NOTIF_OPEN && pl>=sizeof(m_notif_open_t)) {
|
||||
m_notif_open_t o; memcpy(&o,pay,sizeof o);
|
||||
if (strcmp(o.app_id,"os.meow.hello")==0 && o.event_seq==7 && o.origin_x==3 &&
|
||||
o.origin_y==8 && o.hotzone==28 && o.flags==0) {
|
||||
printf("PASS notif_open: type=0x%04x app=%s seq=%u oy=%u hz=%u\n", t, o.app_id, o.event_seq, o.origin_y, o.hotzone);
|
||||
} else { printf("FAIL notif_open: field mismatch\n"); fail=1; }
|
||||
} else { printf("FAIL notif_open: unexpected frame type=0x%04x plen=%u\n", t, pl); fail=1; }
|
||||
|
||||
/* 2) meow_ui_sys_notif_pull(5):顶部下滑系统手势 → SDK 上报 MSG_NOTIF_OPEN(不渲染系统 UI)。
|
||||
* app_id 走 env MEOW_LOG_APP;hotzone=状态栏激活(28);fullscreen=0 → flags=0。 */
|
||||
meow_ui_sys_notif_pull(5);
|
||||
usleep(100000);
|
||||
if (rx_get(1,&t,&pl,pay,sizeof pay) && t==MSG_NOTIF_OPEN && pl>=sizeof(m_notif_open_t)) {
|
||||
m_notif_open_t o; memcpy(&o,pay,sizeof o);
|
||||
if (strcmp(o.app_id,"os.meow.hello")==0 && o.origin_y==5 && o.hotzone==28 && o.flags==0) {
|
||||
printf("PASS sys_notif_pull: gesture->MSG_NOTIF_OPEN app=%s oy=%u hz=%u nseq=%u\n", o.app_id, o.origin_y, o.hotzone, o.event_seq);
|
||||
} else { printf("FAIL sys_notif_pull: field mismatch\n"); fail=1; }
|
||||
} else { printf("FAIL sys_notif_pull: unexpected frame type=0x%04x plen=%u\n", t, pl); fail=1; }
|
||||
|
||||
/* 3) meow_notify_ipc_notif_close 发 MSG_NOTIF_CLOSE */
|
||||
meow_notify_ipc_notif_close("os.meow.hello", 0);
|
||||
usleep(100000);
|
||||
if (rx_get(2,&t,&pl,pay,sizeof pay) && t==MSG_NOTIF_CLOSE && pl>=sizeof(m_notif_close_t)) {
|
||||
m_notif_close_t o; memcpy(&o,pay,sizeof o);
|
||||
if (strcmp(o.app_id,"os.meow.hello")==0 && o.reason==0) {
|
||||
printf("PASS notif_close: type=0x%04x app=%s reason=%u\n", t, o.app_id, o.reason);
|
||||
} else { printf("FAIL notif_close: field mismatch\n"); fail=1; }
|
||||
} else { printf("FAIL notif_close: unexpected frame type=0x%04x plen=%u\n", t, pl); fail=1; }
|
||||
|
||||
/* 4) 帧组装解析 round-trip:构造 m_notif_open_t 序列化 → 解析恢复各字段。 */
|
||||
{
|
||||
m_notif_open_t a; memset(&a,0,sizeof a);
|
||||
snprintf(a.app_id,sizeof a.app_id,"os.meow.hello");
|
||||
a.event_seq=9; a.origin_x=2; a.origin_y=32; a.hotzone=4; a.flags=1;
|
||||
m_notif_open_t b; memset(&b,0,sizeof b);
|
||||
memcpy(&b,&a,sizeof a);
|
||||
if (b.event_seq==9 && b.origin_y==32 && b.hotzone==4 && b.flags==1 && b.app_id[0]=='o')
|
||||
printf("PASS roundtrip: m_notif_open_t 44B (sz=%zu)\n", sizeof a);
|
||||
else { printf("FAIL roundtrip\n"); fail=1; }
|
||||
}
|
||||
|
||||
/* 5) meow_notify_ipc_send_raw want_ack 路径(无效帧类型,服务端回 ERR? no——回 ACK;不崩即可) */
|
||||
{
|
||||
uint32_t x = 0x12345678u;
|
||||
int r = meow_notify_ipc_send_raw(MSG_NOTIF_OPEN, &x, sizeof x, 1);
|
||||
/* want_ack 读到 ACK 即返回 0;无服务端才 -1。这里服务端在跑,应为 0。 */
|
||||
printf("%s send_raw(MSG_NOTIF_OPEN, want_ack) -> %d\n", r==0?"PASS":"FAIL", r);
|
||||
if (r!=0) fail=1;
|
||||
}
|
||||
|
||||
g_stop = 1;
|
||||
pthread_join(th, NULL); /* 服务端线程自关 lfd 并退出 */
|
||||
|
||||
if (fail) { printf("RESULT: FAIL\n"); return 1; }
|
||||
printf("RESULT: ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* test_notify_integration.c — 宿主集成测试:meow_notify_send/cancel 全链路(任务 #5)。
|
||||
*
|
||||
* 直接把 meow_ui_notify.c + meow_notify_ipc.c 编进来,仅 stub 两个 SDK 能力
|
||||
* (meow_log 空实现 / meow_ui_time_ms 单调毫秒),用本地 AF_UNIX stub socket 回环:
|
||||
*
|
||||
* S1 send(general,timeout=0) → 返回本地 id(1),stub 回 nid=101,active_count=1;
|
||||
* S2 send(media) → media=1、category=media(经 stub 捕获帧校验);
|
||||
* S3 同类型再发(未满配额)→ 返回本地 id(3);
|
||||
* S4 cancel(本地id=1) → 返回 1,stub 收到 MSG_NOTIFY_DISMISS(负载 nid=101);
|
||||
* S5 配额满(每类型4)后发第 5 条 → 丢最旧:该 5 条 send 的 replaces_id == 被丢最旧的 nid;
|
||||
* S6 无 launcher(connect 失败)→ send 仍返回本地 id(>0)、不崩;cancel 也静默不崩。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_notify_integration test_notify_integration.c \
|
||||
* meow_ui_notify.c meow_notify_ipc.c -I. -I../meow_launcherd -lpthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_notify_ipc.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ---- stub SDK 依赖(避免链整库) ---- */
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; }
|
||||
uint64_t meow_ui_time_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
/* ---- stub server 捕获(每次连接一帧) ---- */
|
||||
#define MAXCAP 16
|
||||
typedef struct { int type; m_notify_t n; m_notify_progress_t prog; uint32_t dismiss_id; uint32_t nid; } srec_t;
|
||||
static srec_t g_rec[MAXCAP];
|
||||
static int g_nrec = 0;
|
||||
static int g_stop = 0;
|
||||
static int g_listen_fd = -1;
|
||||
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void *server(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
while (!g_stop) {
|
||||
int c = accept(g_listen_fd, NULL, NULL);
|
||||
if (c < 0) {
|
||||
if (errno == EAGAIN && g_stop) break; /* 非阻塞:listener 被关闭 */
|
||||
if (errno == EBADF) break;
|
||||
usleep(1000);
|
||||
continue;
|
||||
}
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
size_t off = 0;
|
||||
while (off < sizeof hdr) { ssize_t r = read(c, hdr + off, sizeof hdr - off); if (r <= 0) break; off += (size_t)r; }
|
||||
if (off < sizeof hdr) { close(c); continue; }
|
||||
uint32_t type, len, req_id;
|
||||
memcpy(&req_id, hdr + 8, 4);
|
||||
memcpy(&len, hdr + 12, 4);
|
||||
memcpy(&type, hdr + 6, 2);
|
||||
if (len > MEOW_IPC_MAX) { close(c); continue; }
|
||||
uint8_t pay[MEOW_IPC_MAX];
|
||||
off = 0;
|
||||
while (off < len) { ssize_t r = read(c, pay + off, len - off); if (r <= 0) break; off += (size_t)r; }
|
||||
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int idx = g_nrec < MAXCAP ? g_nrec++ : (MAXCAP - 1);
|
||||
memset(&g_rec[idx], 0, sizeof g_rec[idx]);
|
||||
g_rec[idx].type = (int)type;
|
||||
if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) memcpy(&g_rec[idx].n, pay, sizeof(m_notify_t));
|
||||
if (type == MSG_NOTIFY_PROGRESS && len >= sizeof(m_notify_progress_t)) memcpy(&g_rec[idx].prog, pay, sizeof(m_notify_progress_t));
|
||||
if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_rec[idx].dismiss_id, pay, 4);
|
||||
/* 分配 nid = 100 + NOTIFY 序号 */
|
||||
uint32_t seq = 0;
|
||||
for (int i = 0; i <= idx; i++) if (g_rec[i].type == MSG_NOTIFY) seq++;
|
||||
g_rec[idx].nid = (type == MSG_NOTIFY) ? (100 + seq) : 0;
|
||||
uint32_t ack_nid = g_rec[idx].nid;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
|
||||
if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS || type == MSG_NOTIFY_PROGRESS) {
|
||||
m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; a.status = ack_nid;
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t mag = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION, t = (uint16_t)
|
||||
(type == MSG_NOTIFY ? MSG_NOTIFY_ACK : MSG_ACK); uint32_t rl = sizeof a;
|
||||
memcpy(rhdr+0,&mag,4); memcpy(rhdr+4,&ver,2); memcpy(rhdr+6,&t,2); memcpy(rhdr+8,&req_id,4); memcpy(rhdr+12,&rl,4);
|
||||
size_t w0 = 0; while (w0 < sizeof rhdr) { ssize_t w = write(c, rhdr+w0, sizeof rhdr-w0); if (w<=0) break; w0 += (size_t)w; }
|
||||
size_t w1 = 0; while (w1 < rl) { ssize_t w = write(c, (uint8_t*)&a+w1, rl-w1); if (w<=0) break; w1 += (size_t)w; }
|
||||
}
|
||||
close(c);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int g_listen_made = 0;
|
||||
static int make_listen(char *path, size_t n)
|
||||
{
|
||||
snprintf(path, n, "/tmp/meow_notify_intg_%d.sock", (int)getpid());
|
||||
unlink(path);
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
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 (bind(fd, (struct sockaddr*)&a, sizeof a) < 0 || listen(fd, 4) < 0) { close(fd); return -1; }
|
||||
fcntl(fd, F_SETFL, O_NONBLOCK); /* 非阻塞 listener:accept() 可被 g_stop 截断 */
|
||||
g_listen_fd = fd; g_listen_made = 1;
|
||||
return 0;
|
||||
}
|
||||
static void stop_server(void)
|
||||
{
|
||||
if (g_listen_made) { g_stop = 1; close(g_listen_fd); g_listen_made = 0; }
|
||||
}
|
||||
|
||||
static srec_t rec_at(int i)
|
||||
{
|
||||
srec_t r; pthread_mutex_lock(&g_mu); r = g_rec[i]; pthread_mutex_unlock(&g_mu); return r;
|
||||
}
|
||||
|
||||
static int failures = 0;
|
||||
static void check(const char *name, int cond)
|
||||
{
|
||||
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (!cond) failures++;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_LOG_APP", "intgapp", 1);
|
||||
char sock[108];
|
||||
if (make_listen(sock, sizeof sock) != 0) { fprintf(stderr, "cannot listen\n"); return 2; }
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
pthread_t th; pthread_create(&th, NULL, server, NULL);
|
||||
|
||||
/* S1 通用通知 */
|
||||
meow_notify nt; memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.urgency = MEOW_NOTIFY_URG_NORMAL;
|
||||
nt.title = "t1"; nt.timeout_ms = 0; nt.icon = "os.meow.hello/icon.png";
|
||||
uint32_t id1 = meow_notify_send(&nt);
|
||||
check("S1 send 返回本地 id=1", id1 == 1);
|
||||
check("S1 active_count=1", meow_notify_active_count() == 1);
|
||||
check("S1 stub 回 nid=101", rec_at(0).nid == 101);
|
||||
check("S1 映射 urgency=NORMAL(1)", rec_at(0).n.urgency == MEOW_NOTIFY_URG_NORMAL);
|
||||
check("S1 映射 icon→app_icon", !strcmp(rec_at(0).n.app_icon, "os.meow.hello/icon.png"));
|
||||
|
||||
/* S2 媒体通知:单例 + media=1 */
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_MEDIA; nt.title = "music"; nt.timeout_ms = -1;
|
||||
uint32_t id2 = meow_notify_send(&nt);
|
||||
check("S2 媒体返本地 id=2", id2 == 2);
|
||||
check("S2 category=media", !strcmp(rec_at(1).n.category, "media"));
|
||||
check("S2 media=1", rec_at(1).n.media == 1);
|
||||
check("S2 expire=0xFFFFFFFF", rec_at(1).n.expire_timeout == 0xFFFFFFFFu);
|
||||
|
||||
/* S3 同类型再发(general 未满 4)→ 新本地 id */
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.title = "t3"; nt.timeout_ms = 0;
|
||||
uint32_t id3 = meow_notify_send(&nt);
|
||||
check("S3 再发返本地 id=3", id3 == 3);
|
||||
check("S3 第2条general nid=103", rec_at(2).nid == 103);
|
||||
|
||||
/* S4 cancel(id1) → DISMISS nid=101 */
|
||||
int rc = meow_notify_cancel(id1);
|
||||
check("S4 cancel 返回 1", rc == 1);
|
||||
check("S4 stub 收到 MSG_NOTIFY_DISMISS", rec_at(3).type == MSG_NOTIFY_DISMISS);
|
||||
check("S4 dismiss 负载 nid=101", rec_at(3).dismiss_id == 101);
|
||||
check("S4 本地表已撤(count=2)", meow_notify_active_count() == 2);
|
||||
|
||||
/* S5 配额满 → 丢最旧 + replaces_id 复用被丢 nid。
|
||||
当前 general 有 id3(1 条);再发 4 条到 4 条(第 4 条触顶后丢最旧)。 */
|
||||
for (int i = 0; i < 4; i++) {
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_GENERAL; nt.title = "quota"; nt.timeout_ms = 0;
|
||||
(void)meow_notify_send(&nt);
|
||||
}
|
||||
/* 现在 general 应仍有 4 条(第5条丢最旧 id3=本地,即被降级替换) */
|
||||
check("S5 配额满后每类型仍=4", meow_notify_count_in_type(MEOW_NOTIFY_CAT_GENERAL) == 4);
|
||||
/* 被丢最旧 = id3(第2条 general,本地 id=3),其 launcher nid=103(rec[2])。
|
||||
触发丢最旧的是 S5 循环第 4 次 send = 第 7 条 NOTIFY(rec[7]),其 replaces_id==103。 */
|
||||
check("S5 丢最旧后 send replaces_id=被丢nid(103)", rec_at(7).n.replaces_id == 103);
|
||||
|
||||
/* S5b update_progress:新发一条 progress → 本地 id → update → stub 捕获 MSG_NOTIFY_PROGRESS */
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_PROGRESS; nt.title = "prog"; nt.timeout_ms = 0;
|
||||
nt.progress = 10;
|
||||
uint32_t idp = meow_notify_send(&nt);
|
||||
int ni = -1;
|
||||
for (int i = 0; i < MAXCAP; i++) {
|
||||
srec_t r = rec_at(i);
|
||||
if (r.type == MSG_NOTIFY && !strcmp(r.n.summary, "prog")) { ni = i; break; }
|
||||
}
|
||||
check("S5b 找到 prog 通知帧", ni >= 0);
|
||||
int rc_up = meow_notify_update_progress(idp, 70);
|
||||
check("S5b update_progress 返回 1", rc_up == 1);
|
||||
srec_t pr = rec_at(ni + 1);
|
||||
check("S5b stub 捕获 MSG_NOTIFY_PROGRESS 帧", pr.type == MSG_NOTIFY_PROGRESS);
|
||||
check("S5b progress nid=该通知 launcher nid", ni >= 0 && pr.prog.nid == rec_at(ni).nid);
|
||||
check("S5b progress 载=70", pr.prog.progress == 70);
|
||||
check("S5b update_progress(未知 id) 返回 0", meow_notify_update_progress(0xFFFFFF, 50) == 0);
|
||||
|
||||
stop_server();
|
||||
pthread_join(th, NULL);
|
||||
|
||||
/* S6 无 launcher 降级 */
|
||||
char dead[108]; snprintf(dead, sizeof dead, "/tmp/meow_notify_intg_dead_%d.sock", (int)getpid());
|
||||
unlink(dead);
|
||||
setenv("MEOW_IPC_SOCK", dead, 1);
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_EMAIL; nt.title = "no-launcher"; nt.timeout_ms = 0;
|
||||
uint32_t id6 = meow_notify_send(&nt);
|
||||
check("S6 无 launcher 仍返回本地 id(>0)", id6 > 0);
|
||||
check("S6 无 launcher active_count 正常", meow_notify_active_count() >= 1);
|
||||
check("S6 cancel 无 launcher 静默不崩", meow_notify_cancel(id6) == 1);
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* test_notify_ipc.c — 宿主测试:任务 #5 SDK↔IPC 桥(meow_notify_ipc.{h,c})。
|
||||
*
|
||||
* 用本地 AF_UNIX stub socket(回环)验证:
|
||||
* T1 枚举↔字符串映射:CAT_PROGRESS→"progress"、CAT_MEDIA→"media" + media=1;
|
||||
* T2 timeout 映射:-1(ONGOING)→expire=0xFFFFFFFF + ongoing=1;0(NEVER)→expire=0;
|
||||
* >0→该毫秒 + ongoing=0;
|
||||
* T3 回读 MSG_NOTIFY_ACK 拿 launcher 分配的 nid;replaces_id 原样透传;progress 钳位;
|
||||
* T4 无 socket 降级:connect 失败 → meow_notify_ipc_send 返回 0,不崩;
|
||||
* T5 dismiss:meow_notify_ipc_dismiss(nid) 发出 MSG_NOTIFY_DISMISS 帧(负载=nid)。
|
||||
*
|
||||
* 编译(宿主 gcc,不依赖 meow_ui.a 其余 TU):
|
||||
* gcc -o test_notify_ipc test_notify_ipc.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -lpthread
|
||||
* 运行:./test_notify_ipc (全部 PASS 退出码 0,任一 FAIL 退出码 1)
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ipc.h" /* 帧头 / m_notify_t / m_ack_t / MSG_*(同源校验) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ---- 捕获:stub 服务端解析到的帧内容(mutex 保护;客户端返回后读取) ---- */
|
||||
typedef struct {
|
||||
uint32_t type; /* 收到的帧类型 */
|
||||
m_notify_t n; /* MSG_NOTIFY 负载 */
|
||||
m_notify_progress_t prog; /* MSG_NOTIFY_PROGRESS 负载 */
|
||||
uint32_t dismiss_id; /* MSG_NOTIFY_DISMISS 负载 */
|
||||
uint32_t req_id;
|
||||
uint32_t nid_to_return; /* ACK 中回填的 status */
|
||||
int got; /* 已收到一帧 */
|
||||
} cap_t;
|
||||
static cap_t g_cap;
|
||||
static pthread_mutex_t g_cap_mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void cap_store(uint32_t type, uint32_t req_id, const uint8_t *pay, uint32_t len,
|
||||
uint32_t nid_to_return)
|
||||
{
|
||||
pthread_mutex_lock(&g_cap_mu);
|
||||
memset(&g_cap, 0, sizeof g_cap);
|
||||
g_cap.type = type;
|
||||
g_cap.req_id = req_id;
|
||||
g_cap.nid_to_return = nid_to_return;
|
||||
if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) memcpy(&g_cap.n, pay, sizeof(m_notify_t));
|
||||
if (type == MSG_NOTIFY_PROGRESS && len >= sizeof(m_notify_progress_t)) memcpy(&g_cap.prog, pay, sizeof(m_notify_progress_t));
|
||||
if (type == MSG_NOTIFY_DISMISS && len >= 4) memcpy(&g_cap.dismiss_id, pay, 4);
|
||||
g_cap.got = 1;
|
||||
pthread_mutex_unlock(&g_cap_mu);
|
||||
}
|
||||
|
||||
/* ---- stub 服务端线程:accept 一个连接 → 读一帧 → 校验由主线程做,这里只收 + 回 ACK ---- */
|
||||
static int g_listen_fd = -1;
|
||||
static void *server_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
int c = accept(g_listen_fd, NULL, NULL);
|
||||
if (c < 0) return NULL;
|
||||
/* 读 16B 头 */
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t off = 0;
|
||||
while (off < sizeof hdr) {
|
||||
ssize_t r = read(c, hdr + off, sizeof hdr - off);
|
||||
if (r <= 0) { close(c); return NULL; }
|
||||
off += (size_t)r;
|
||||
}
|
||||
uint32_t magic, len;
|
||||
uint16_t ver, type;
|
||||
memcpy(&magic, hdr, 4);
|
||||
memcpy(&ver, hdr + 4, 2);
|
||||
memcpy(&type, hdr + 6, 2);
|
||||
memcpy(&len, hdr + 12, 4);
|
||||
uint32_t req_id; memcpy(&req_id, hdr + 8, 4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) { close(c); return NULL; }
|
||||
if (len > MEOW_IPC_MAX) { close(c); return NULL; }
|
||||
uint8_t pay[MEOW_IPC_MAX];
|
||||
off = 0;
|
||||
while (off < len) {
|
||||
ssize_t r = read(c, pay + off, len - off);
|
||||
if (r <= 0) { close(c); return NULL; }
|
||||
off += (size_t)r;
|
||||
}
|
||||
cap_store(type, req_id, pay, len, (uint32_t)strtoul(getenv("TEST_NID"), NULL, 0));
|
||||
|
||||
/* MSG_NOTIFY → 回 m_ack_t(code=OK, status=nid_to_return);
|
||||
* MSG_NOTIFY_DISMISS → 回 m_ack_t(code=OK, status=0,同 launcher 行为)。 */
|
||||
if (type == MSG_NOTIFY || type == MSG_NOTIFY_DISMISS || type == MSG_NOTIFY_PROGRESS) {
|
||||
m_ack_t a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.code = MEOW_OK;
|
||||
a.status = (type == MSG_NOTIFY) ? (uint32_t)strtoul(getenv("TEST_NID"), NULL, 0) : 0;
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t rmagic = MEOW_IPC_MAGIC;
|
||||
uint16_t rver = MEOW_IPC_VERSION, rtype = (uint16_t)(type == MSG_NOTIFY ? MSG_NOTIFY_ACK : MSG_ACK);
|
||||
uint32_t rlen = sizeof a;
|
||||
memcpy(rhdr + 0, &rmagic, 4);
|
||||
memcpy(rhdr + 4, &rver, 2);
|
||||
memcpy(rhdr + 6, &rtype, 2);
|
||||
memcpy(rhdr + 8, &req_id, 4);
|
||||
memcpy(rhdr + 12, &rlen, 4);
|
||||
off = 0;
|
||||
while (off < sizeof rhdr) { ssize_t w = write(c, rhdr + off, sizeof rhdr - off); if (w <= 0) break; off += (size_t)w; }
|
||||
off = 0;
|
||||
while (off < rlen) { ssize_t w = write(c, (uint8_t*)&a + off, rlen - off); if (w <= 0) break; off += (size_t)w; }
|
||||
}
|
||||
close(c);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static void check(const char *name, int cond)
|
||||
{
|
||||
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (!cond) failures++;
|
||||
}
|
||||
|
||||
/* 建 listening socket,返回 fd;路径写入 sock_path。 */
|
||||
static int arb_listen(char *sock_path, size_t slen)
|
||||
{
|
||||
static int count = 0;
|
||||
snprintf(sock_path, slen, "/tmp/meow_notify_ipc_%d_%d.sock", (int)getpid(), count++);
|
||||
unlink(sock_path);
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) { perror("socket"); return -1; }
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, sock_path, sizeof a.sun_path - 1);
|
||||
if (bind(fd, (struct sockaddr *)&a, sizeof a) < 0) { perror("bind"); close(fd); return -1; }
|
||||
if (listen(fd, 1) < 0) { perror("listen"); close(fd); return -1; }
|
||||
return fd;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_LOG_APP", "testapp", 1); /* 固定 app_id,供 stub 校验 */
|
||||
|
||||
/* ---------- T1/T2/T3:映射 + ACK 回 nid ---------- */
|
||||
{
|
||||
char sock[108];
|
||||
g_listen_fd = arb_listen(sock, sizeof sock);
|
||||
if (g_listen_fd < 0) { fprintf(stderr, "cannot create listener\n"); return 2; }
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
setenv("TEST_NID", "0x1234", 1);
|
||||
pthread_t th;
|
||||
pthread_create(&th, NULL, server_thread, NULL);
|
||||
|
||||
meow_notify nt;
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_PROGRESS;
|
||||
nt.urgency = MEOW_NOTIFY_URG_HIGH;
|
||||
nt.title = "下载中";
|
||||
nt.body = "70%";
|
||||
nt.icon = "os.meow.hello/icon.png";
|
||||
nt.timeout_ms = 5000; /* >0 → expire=5000, ongoing=0 */
|
||||
nt.progress = 150; /* 钳位 → 100 */
|
||||
uint32_t nid = meow_notify_ipc_send(&nt, 0);
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen_fd);
|
||||
|
||||
check("T3 send 返回 launcher nid(0x1234)", nid == 0x1234);
|
||||
pthread_mutex_lock(&g_cap_mu);
|
||||
int got = g_cap.got;
|
||||
const m_notify_t *cn = &g_cap.n;
|
||||
pthread_mutex_unlock(&g_cap_mu);
|
||||
check("T3 stub 收到 MSG_NOTIFY 帧", got && g_cap.type == MSG_NOTIFY);
|
||||
check("T1 CAT_PROGRESS→category=progress", !strcmp(cn->category, "progress"));
|
||||
check("T2 timeout>0→expire_timeout=5000", cn->expire_timeout == 5000);
|
||||
check("T2 timeout>0→ongoing=0", cn->ongoing == 0);
|
||||
check("T1 media=0(非媒体)", cn->media == 0);
|
||||
check("T1 progress 钳位 150→100", cn->progress == 100);
|
||||
check("T2 replaces_id 透传=0", cn->replaces_id == 0);
|
||||
check("T1 app_id=testapp", !strcmp(cn->app_id, "testapp"));
|
||||
check("T1 summary=下载中", !strcmp(cn->summary, "下载中"));
|
||||
check("T1 urgency(HIGH)→wire urgency=2", cn->urgency == MEOW_NOTIFY_URG_HIGH);
|
||||
check("T1 icon→app_icon", !strcmp(cn->app_icon, "os.meow.hello/icon.png"));
|
||||
}
|
||||
|
||||
/* ---------- T1/T2:媒体类型 + ONGOING(-1) ---------- */
|
||||
{
|
||||
char sock[108];
|
||||
g_listen_fd = arb_listen(sock, sizeof sock);
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
setenv("TEST_NID", "0x9999", 1);
|
||||
pthread_t th;
|
||||
pthread_create(&th, NULL, server_thread, NULL);
|
||||
|
||||
meow_notify nt;
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_MEDIA;
|
||||
nt.urgency = MEOW_NOTIFY_URG_CRITICAL;
|
||||
nt.title = "Now Playing";
|
||||
nt.timeout_ms = MEOW_NOTIFY_TIMEOUT_ONGOING; /* -1 */
|
||||
uint32_t nid = meow_notify_ipc_send(&nt, 0);
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen_fd);
|
||||
|
||||
check("T3 send(media/ongoing) 返回 nid(0x9999)", nid == 0x9999);
|
||||
check("T1 CAT_MEDIA→category=media", !strcmp(g_cap.n.category, "media"));
|
||||
check("T1 CAT_MEDIA→media=1", g_cap.n.media == 1);
|
||||
check("T2 ONGOING(-1)→expire=0xFFFFFFFF", g_cap.n.expire_timeout == 0xFFFFFFFFu);
|
||||
check("T2 ONGOING(-1)→ongoing=1", g_cap.n.ongoing == 1);
|
||||
}
|
||||
|
||||
/* ---------- T2:NEVER(0) 超时 ---------- */
|
||||
{
|
||||
char sock[108];
|
||||
g_listen_fd = arb_listen(sock, sizeof sock);
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
setenv("TEST_NID", "0x1", 1);
|
||||
pthread_t th;
|
||||
pthread_create(&th, NULL, server_thread, NULL);
|
||||
meow_notify nt;
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_GENERAL;
|
||||
nt.title = "x";
|
||||
nt.timeout_ms = MEOW_NOTIFY_TIMEOUT_NEVER; /* 0 */
|
||||
(void)meow_notify_ipc_send(&nt, 0);
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen_fd);
|
||||
check("T2 NEVER(0)→expire=0 & ongoing=0",
|
||||
g_cap.n.expire_timeout == 0 && g_cap.n.ongoing == 0);
|
||||
check("T1 CAT_GENERAL→category=general", !strcmp(g_cap.n.category, "general"));
|
||||
}
|
||||
|
||||
/* ---------- T3:replaces_id 透传 ---------- */
|
||||
{
|
||||
char sock[108];
|
||||
g_listen_fd = arb_listen(sock, sizeof sock);
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
setenv("TEST_NID", "0xBEEF", 1);
|
||||
pthread_t th;
|
||||
pthread_create(&th, NULL, server_thread, NULL);
|
||||
meow_notify nt;
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_MESSAGE;
|
||||
nt.title = "repl";
|
||||
nt.timeout_ms = 0;
|
||||
(void)meow_notify_ipc_send(&nt, 0x42u);
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen_fd);
|
||||
check("T3 replaces_id 透传=0x42", g_cap.n.replaces_id == 0x42u);
|
||||
}
|
||||
|
||||
/* ---------- T4:无 socket 降级不崩 ---------- */
|
||||
{
|
||||
/* 指向不存在路径 */
|
||||
char dead[108];
|
||||
snprintf(dead, sizeof dead, "/tmp/meow_notify_ipc_dead_%d.sock", (int)getpid());
|
||||
unlink(dead);
|
||||
setenv("MEOW_IPC_SOCK", dead, 1);
|
||||
meow_notify nt;
|
||||
memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_EMAIL;
|
||||
nt.title = "no-socket";
|
||||
nt.timeout_ms = 0;
|
||||
uint32_t nid = meow_notify_ipc_send(&nt, 0);
|
||||
check("T4 无 socket→返回 0(降级不崩)", nid == 0);
|
||||
meow_notify_ipc_dismiss(0x1234); /* 无 socket 静默,不崩 */
|
||||
check("T4 dismiss 无 socket 静默不崩", 1);
|
||||
}
|
||||
|
||||
/* ---------- T5:dismiss 发 MSG_NOTIFY_DISMISS ---------- */
|
||||
{
|
||||
char sock[108];
|
||||
g_listen_fd = arb_listen(sock, sizeof sock);
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
pthread_t th;
|
||||
pthread_create(&th, NULL, server_thread, NULL);
|
||||
meow_notify_ipc_dismiss(0xABCDu);
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen_fd);
|
||||
check("T5 stub 收到 MSG_NOTIFY_DISMISS 帧", g_cap.got && g_cap.type == MSG_NOTIFY_DISMISS);
|
||||
check("T5 dismiss 负载=nid=0xABCD", g_cap.dismiss_id == 0xABCDu);
|
||||
}
|
||||
|
||||
/* ---------- T6:update_progress 发 MSG_NOTIFY_PROGRESS(nid + progress 钳位)---------- */
|
||||
{
|
||||
char sock[108];
|
||||
g_listen_fd = arb_listen(sock, sizeof sock);
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
pthread_t th;
|
||||
pthread_create(&th, NULL, server_thread, NULL);
|
||||
meow_notify_ipc_update_progress(0x1234, 250); /* 钳位 250→100 */
|
||||
pthread_join(th, NULL);
|
||||
close(g_listen_fd);
|
||||
check("T6 stub 收到 MSG_NOTIFY_PROGRESS 帧", g_cap.got && g_cap.type == MSG_NOTIFY_PROGRESS);
|
||||
check("T6 progress 负载 nid=0x1234", g_cap.prog.nid == 0x1234);
|
||||
check("T6 progress 钳位 250→100", g_cap.prog.progress == 100);
|
||||
}
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* test_notify_post.c — 宿主交叉验证:SDK send 的 IPC 帧 → launcher 真·中央存储。
|
||||
*
|
||||
* 把 SDK 的 meow_notify_ipc.c(映射+传输)与 launcher 的 meow_notif.c(真·中央存储,
|
||||
* meow_notif_post/dismiss)接到一起,stub 掉 meow_settings 后端;服务端复刻
|
||||
* meow_launcherd.c:6427-6440 的 m_notify_t→meow_notif_t 映射 + MSG_NOTIFY_ACK 应答。
|
||||
*
|
||||
* 验证:SDK meow_notify_ipc_send() 产出的帧被 meow_notif_post() 真实落库并回分配 nid>0;
|
||||
* 随后 meow_notify_ipc_dismiss() 经 meow_notif_dismiss() 撤下(count 归 0)。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_notify_post test_notify_post.c meow_notify_ipc.c \
|
||||
* ../meow_launcherd/meow_notif.c -I. -I../meow_launcherd -lpthread
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ipc.h"
|
||||
#include "meow_notif.h" /* launcher 真·中央存储 */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* ---- stub:settings 后端(键缺省 → meow_notif.c 用默认配额) ---- */
|
||||
int meow_settings_get_int(const char *key, int *out) { (void)key; (void)out; return MEOW_ERR_NOENT; }
|
||||
|
||||
/* ---- 复刻 launcher 的 m_notify_t → meow_notif_t 映射(meow_launcherd.c:6427-6440) ----
|
||||
* 注:与 launcher 同款 snprintf(dest 较 src 小会有 format-truncation 提示),launcher 自身
|
||||
* 以其编译 flags 构建无警告;此处仅为宿主测试,用 pragma 压掉以免 -Werror 干扰。 */
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wformat-truncation"
|
||||
#endif
|
||||
static void launcher_map(const m_notify_t *mn, meow_notif_t *n)
|
||||
{
|
||||
memset(n, 0, sizeof *n);
|
||||
n->id = mn->replaces_id; /* 0=>新建;>0=>替换(不存在则新建) */
|
||||
snprintf(n->app_id, sizeof n->app_id, "%s", mn->app_id);
|
||||
snprintf(n->category, sizeof n->category, "%s", mn->category);
|
||||
snprintf(n->summary, sizeof n->summary, "%s", mn->summary);
|
||||
snprintf(n->body, sizeof n->body, "%s", mn->body);
|
||||
snprintf(n->app_icon, sizeof n->app_icon, "%s", mn->app_icon);
|
||||
snprintf(n->target_activity, sizeof n->target_activity, "%s", mn->target_activity);
|
||||
n->expire_timeout = mn->expire_timeout;
|
||||
n->progress = mn->progress;
|
||||
n->urgency = mn->urgency; /* 0-3;旧调用=0 → LOW(静默) */
|
||||
n->ongoing = mn->ongoing;
|
||||
n->media = mn->media;
|
||||
}
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
static int g_listen_fd = -1;
|
||||
static int g_stop = 0;
|
||||
static uint32_t g_posted_nid = 0;
|
||||
static meow_notif_t g_stored; /* 回读 launcher 存储的最近一条通知(store_cb) */
|
||||
static int g_store_called = 0;
|
||||
|
||||
static int store_cb(const meow_notif_t *n, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (n) { g_stored = *n; g_store_called = 1; }
|
||||
return 1; /* 只取第一条 */
|
||||
}
|
||||
|
||||
static void reply_ack(int c, uint16_t type, uint32_t req_id, uint32_t status)
|
||||
{
|
||||
m_ack_t a; memset(&a, 0, sizeof a); a.code = MEOW_OK; a.status = status;
|
||||
uint8_t rhdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t mag = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION, t = type; uint32_t rl = sizeof a;
|
||||
memcpy(rhdr+0,&mag,4); memcpy(rhdr+4,&ver,2); memcpy(rhdr+6,&t,2); memcpy(rhdr+8,&req_id,4); memcpy(rhdr+12,&rl,4);
|
||||
size_t o = 0; while (o < sizeof rhdr) { ssize_t w = write(c, rhdr+o, sizeof rhdr-o); if (w<=0) break; o += (size_t)w; }
|
||||
o = 0; while (o < rl) { ssize_t w = write(c, (uint8_t*)&a+o, rl-o); if (w<=0) break; o += (size_t)w; }
|
||||
}
|
||||
|
||||
static void *server(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
meow_notif_init();
|
||||
while (!g_stop) {
|
||||
int c = accept(g_listen_fd, NULL, NULL);
|
||||
if (c < 0) { if (errno == EAGAIN && g_stop) break; if (errno == EBADF) break; usleep(1000); continue; }
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN]; size_t o = 0;
|
||||
while (o < sizeof hdr) { ssize_t r = read(c, hdr+o, sizeof hdr-o); if (r<=0) break; o += (size_t)r; }
|
||||
if (o < sizeof hdr) { close(c); continue; }
|
||||
uint32_t type, len, req_id; memcpy(&type, hdr+6, 2); memcpy(&req_id, hdr+8, 4); memcpy(&len, hdr+12, 4);
|
||||
if (len > MEOW_IPC_MAX) { close(c); continue; }
|
||||
uint8_t pay[MEOW_IPC_MAX]; o = 0;
|
||||
while (o < len) { ssize_t r = read(c, pay+o, len-o); if (r<=0) break; o += (size_t)r; }
|
||||
|
||||
if (type == MSG_NOTIFY && len >= sizeof(m_notify_t)) {
|
||||
m_notify_t mn; memcpy(&mn, pay, sizeof mn);
|
||||
meow_notif_t n; launcher_map(&mn, &n);
|
||||
uint32_t nid = 0;
|
||||
int rc = meow_notif_post(&n, &nid);
|
||||
if (rc == NOTIF_OK) g_posted_nid = nid;
|
||||
reply_ack(c, MSG_NOTIFY_ACK, req_id, (rc == NOTIF_OK) ? nid : (uint32_t)rc);
|
||||
fprintf(stderr, "[post-srv] post rc=%d nid=%u cat=%s\n", rc, nid, mn.category);
|
||||
} else if (type == MSG_NOTIFY_DISMISS && len >= 4) {
|
||||
uint32_t id; memcpy(&id, pay, 4);
|
||||
int rc = meow_notif_dismiss(id);
|
||||
reply_ack(c, MSG_ACK, req_id, 0);
|
||||
fprintf(stderr, "[post-srv] dismiss id=%u rc=%d\n", id, rc);
|
||||
}
|
||||
close(c);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int make_listen(char *path, size_t n)
|
||||
{
|
||||
snprintf(path, n, "/tmp/meow_notify_post_%d.sock", (int)getpid());
|
||||
unlink(path);
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
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 (bind(fd, (struct sockaddr*)&a, sizeof a) < 0 || listen(fd, 4) < 0) { close(fd); return -1; }
|
||||
fcntl(fd, F_SETFL, O_NONBLOCK);
|
||||
g_listen_fd = fd;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int failures = 0;
|
||||
static void check(const char *name, int cond) { printf("%s %s\n", cond ? "PASS" : "FAIL", name); if (!cond) failures++; }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_LOG_APP", "intgapp", 1);
|
||||
char sock[108];
|
||||
if (make_listen(sock, sizeof sock) != 0) { fprintf(stderr, "cannot listen\n"); return 2; }
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
pthread_t th; pthread_create(&th, NULL, server, NULL);
|
||||
|
||||
/* SDK send → launcher 真存储。media + ONGOING 走全部映射分支。 */
|
||||
meow_notify nt; memset(&nt, 0, sizeof nt);
|
||||
nt.cat = MEOW_NOTIFY_CAT_MEDIA; nt.urgency = MEOW_NOTIFY_URG_HIGH;
|
||||
nt.title = "正在播放"; nt.timeout_ms = -1; nt.progress = 40;
|
||||
nt.icon = "os.meow.hello/icon.png";
|
||||
uint32_t nid = meow_notify_ipc_send(&nt, 0);
|
||||
check("send 经 launcher 真存储回 nid>0", nid != 0);
|
||||
check("send 拿到 launcher 分配的 nid(与 post 一致)", g_posted_nid == nid);
|
||||
check("launcher 存储 count(media app)=1", meow_notif_count("intgapp") == 1);
|
||||
|
||||
/* 回读 launcher 存储,验证 urgency/icon/progress 全链路落库 */
|
||||
g_store_called = 0; memset(&g_stored, 0, sizeof g_stored);
|
||||
meow_notif_list(NULL, store_cb, NULL);
|
||||
check("post 存储 urgency=HIGH(2)", g_store_called && g_stored.urgency == MEOW_NOTIFY_URG_HIGH);
|
||||
check("post 存储 icon→app_icon", g_stored.app_icon[0] && !strcmp(g_stored.app_icon, "os.meow.hello/icon.png"));
|
||||
check("post 存储 progress=40", g_stored.progress == 40);
|
||||
|
||||
/* SDK dismiss → launcher 撤下 */
|
||||
meow_notify_ipc_dismiss(nid);
|
||||
check("dismiss 后 launcher 存储 count=0", meow_notif_count("intgapp") == 0);
|
||||
|
||||
g_stop = 1; close(g_listen_fd);
|
||||
pthread_join(th, NULL);
|
||||
meow_notif_shutdown();
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS", failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/* test_overlay_suppress.c — §10.5 叠加让位 present 自抑宿主验证(离屏 480x720,16bpp)。
|
||||
*
|
||||
* 背景:媒体播放中的 App 前台唤起通知中心时 launcher 不 SIGSTOP(保 PCM 供流),改由 SDK
|
||||
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
|
||||
* T2 有标记:flush 被抑制(只画进 back_buf 不 present:frames 不变、抑制计数递增);
|
||||
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
|
||||
* T3 标记消失:下一次 flush 恢复上屏(抑制期累积脏区一次上屏,无需重画);
|
||||
* 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
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#define MARKER "/tmp/test_notif_active_sdk"
|
||||
|
||||
static void marker_write(void)
|
||||
{
|
||||
int fd = open(MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
assert(fd >= 0);
|
||||
if (write(fd, "1", 1) < 0) assert(0);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/overlay_suppress.ppm", 1);
|
||||
setenv("MEOW_NOTIF_ACTIVE_FILE", MARKER, 1);
|
||||
unlink(MARKER);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
meow_ui_set_refresh_always_max(1); /* 绕过帧率门控:每次 flush 都直达 present 判定 */
|
||||
|
||||
/* T1:无标记正常上屏(默认 App 进程模式:render_enabled==0) */
|
||||
assert(meow_ui_sys_render_enabled() == 0);
|
||||
meow_ui_clear(0x112233);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 0);
|
||||
printf("T1 PASS: no marker -> presented=1 suppressed=0\n");
|
||||
|
||||
/* T2:有标记 → 抑制(等 >200ms 越过存在性节流缓存) */
|
||||
marker_write();
|
||||
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);
|
||||
|
||||
/* T2b:抑制期再次 flush 仍抑制(节流缓存内一致) */
|
||||
meow_ui_clear(0x334455);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f0);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 2);
|
||||
printf("T2b PASS: still suppressed within throttle window\n");
|
||||
|
||||
/* T3:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏,无需重画) */
|
||||
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");
|
||||
|
||||
/* T4:系统 UI 进程(launcher)不抑制——通知中心面板本身必须照常上屏 */
|
||||
meow_ui_sys_set_system_mode(1);
|
||||
assert(meow_ui_sys_render_enabled() == 1);
|
||||
marker_write();
|
||||
usleep(250000);
|
||||
unsigned long long f1 = meow_ui_frames_presented();
|
||||
meow_ui_clear(0x445566);
|
||||
meow_ui_flush();
|
||||
assert(meow_ui_frames_presented() == f1 + 1);
|
||||
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长 */
|
||||
unlink(MARKER);
|
||||
printf("T4 PASS: system-UI process (launcher) never suppressed\n");
|
||||
|
||||
meow_ui_shutdown();
|
||||
printf("ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* test_p1p2p3_repro.c — 宿主复现并校验 P1/P2/P3 三个真机问题的修复(离屏 480x720,16bpp)。
|
||||
*
|
||||
* 方法:直接驱动拆分后 hello 的 demo 真实函数(demo_paint / demo_rebuild_to / demo_touch_down)
|
||||
* 与 demo 视图态(extern,见 ../hello_app/hello_internal.h),对 back_buf 做像素断言。
|
||||
* 这样测的是「真实 demo 代码 + 真实 SDK」,不是模拟桩。不依赖 /dev/fb0 / 字库 / launcher。
|
||||
*
|
||||
* 批次② 变化:hello.c 已拆为 app.c + ui/{home,full,demo}.c + ui/common/chrome.c +
|
||||
* logic/{notify,state}.c;本文件不再 #include hello.c,改为 extern 驱动 demo 层并链接多文件。
|
||||
* 批次③ 变化:ui/home、ui/demo 子目录化(ui/home/home.c + ui/demo/demo.c),demo 列表区块抽成
|
||||
* CONTAINER 子视图 ui/demo/list/list.c;下表编译路径相应更新(断言未弱化)。
|
||||
*
|
||||
* 覆盖:
|
||||
* P1 z-order 叠层:圆形进度条(实时重绘)与 FAB 浮层重叠时,局部重绘不再清掉/盖住 FAB。
|
||||
* (批次① 后为 P1c:set_z(g_fab,999)+invalidate(r_prc),显式 z 不依赖绘制先后。)
|
||||
* 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 拆分层):
|
||||
* 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
|
||||
* 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_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 \
|
||||
* ../hello_app/ui/demo/list/list.c ../hello_app/ui/common/chrome.c \
|
||||
* ../hello_app/logic/notify.c ../hello_app/logic/state.c \
|
||||
* -I. -I../hello_app -I../meow_launcherd -pthread -lm
|
||||
* 运行:MEOW_UI_DUMP=/tmp/repro.ppm ./test_p1p2p3_repro (optional,dump 最终屏幕)
|
||||
*
|
||||
* 预期 RESULT: ALL PASS(P1c/P2/P3 断言真实有效,未弱化)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "../hello_app/hello_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.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) { *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 near(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 12; }
|
||||
static int is_bg(int r, int g, int b)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
int br = (th->bg >> 16) & 0xFF, bg = (th->bg >> 8) & 0xFF, bb = th->bg & 0xFF;
|
||||
return near(r, br) && near(g, bg) && near(b, bb);
|
||||
}
|
||||
static void pr_px(const char *tag, int x, int y)
|
||||
{
|
||||
int r, g, b; rd_px(x, y, &r, &g, &b);
|
||||
printf(" %-26s px(%d,%d)=%02X%02X%02X %s\n", tag, x, y, r, g, b, is_bg(r, g, b) ? "[bg]" : "[ink]");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/repro.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
meow_ui_activity_push(ACT_DEMO); /* demo 控件 owner=ACT_DEMO,命中/绘制对齐 */
|
||||
demo_init(); /* 建造 chrome + 内容(scroll=0),owner=ACT_DEMO */
|
||||
|
||||
int fails = 0;
|
||||
|
||||
/* ================= P1 / P1c:局部重绘不盖浮层(FAB,须显式置顶) =================
|
||||
* 场景:scroll=0 时圆形进度 r_prc(413..453,605..645) 与 FAB(406..462,616..672) 重叠。
|
||||
* demo_paint 整帧后 FAB 在上(demo_init set_z(g_fab,999) 显式置顶)。随后「局部重绘进度条」:
|
||||
* 旧(清底+只画进度条+flush 其矩形)→ FAB 重叠区被清掉;
|
||||
* 新(SDK meow_ui_invalidate:清底 + 按 z 重画重叠区完整 z 栈)→ FAB 保持。
|
||||
* P1c:set_z(g_fab,999) 后 invalidate(r_prc),断言 FAB 重叠点(434,622) 仍=demo_paint 后值
|
||||
* (显式 z 不依赖绘制先后——置顶在重绘前显式给出,下层重绘不抹上层)。 */
|
||||
demo_paint();
|
||||
int fx = r_fab.x + 28, fy = r_fab.y + 6; /* FAB 内、远离 "+" 文本处 → (434,622) */
|
||||
int s0r, s0g, s0b; rd_px(fx, fy, &s0r, &s0g, &s0b);
|
||||
printf("P1 scene (demo_paint) FAB-overlap "); pr_px("scene", fx, fy);
|
||||
if (is_bg(s0r, s0g, s0b))
|
||||
printf(" [warn] expected FAB ink at scene, got bg — geometry assumptions off\n");
|
||||
/* 旧式局部重绘:清底+只画进度条(复现根因) */
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_progress_set(g_dprc, 30);
|
||||
meow_ui_fill_rect(r_prc.x, r_prc.y, r_prc.w, r_prc.h, th->bg);
|
||||
meow_widget_draw(g_dprc);
|
||||
meow_ui_flush_region(r_prc.x, r_prc.y, r_prc.w, r_prc.h);
|
||||
}
|
||||
int obr, obg, obb; rd_px(fx, fy, &obr, &obg, &obb);
|
||||
printf("P1 old-partial (clear+single draw) "); pr_px("old", fx, fy);
|
||||
if (is_bg(obr, obg, obb)) printf(" [BUG-CONFIRMED] old path erased FAB overlap (bg)\n");
|
||||
else printf(" [note] old path did NOT erase FAB here (color %02X%02X%02X)\n", obr, obg, obb);
|
||||
|
||||
/* P1c:显式置顶 + SDK 局部重画(invalidate 按 z 重画重叠栈) */
|
||||
demo_paint(); /* 还原场景 */
|
||||
meow_widget_set_z(g_fab, 999); /* 重新显式置顶(P1c 要求) */
|
||||
meow_progress_set(g_dprc, 30);
|
||||
meow_ui_invalidate(r_prc.x, r_prc.y, r_prc.w, r_prc.h);
|
||||
int nbr, nbg, nbb; rd_px(fx, fy, &nbr, &nbg, &nbb);
|
||||
printf("P1c set_z(999)+invalidate(r_prc) "); pr_px("p1c", fx, fy);
|
||||
if (near(s0r, nbr) && near(s0g, nbg) && near(s0b, nbb))
|
||||
printf(" P1-PASS explicit z:FAB preserved over live progress redraw\n");
|
||||
else { printf(" P1-FAIL FAB not preserved (scene %02X%02X%02X vs p1c %02X%02X%02X)\n",
|
||||
s0r, s0g, s0b, nbr, nbg, nbb); fails++; }
|
||||
|
||||
/* ================= P2:列表焦点按本次触摸绑定/释放 =================
|
||||
* demo_touch_down 命中非 list 区域时应释放列表焦点 → 页滚动,而不是继续滚列表。 */
|
||||
g_demo_focus = 1; /* 模拟此前点过列表(焦点留存) */
|
||||
g_demo = 1; /* demo 页已进入(demo_touch_down 前置闸门) */
|
||||
demo_touch_down(240, 300); /* 非 list 非 slider 的内容区 */
|
||||
printf("P2 touch_down(240,300) with focus=1 -> drag_list=%d drag_page=%d focus=%d\n",
|
||||
g_drag_list, g_drag_page, g_demo_focus);
|
||||
if (g_drag_list == 0 && g_drag_page == 1 && g_demo_focus == 0)
|
||||
printf(" P2-PASS non-list touch releases focus, drags page\n");
|
||||
else { printf(" P2-FAIL (expected drag_page=1 focus=0; got drag_list=%d drag_page=%d focus=%d)\n",
|
||||
g_drag_list, g_drag_page, g_demo_focus); fails++; }
|
||||
|
||||
/* ================= P3:列表滚动 W_LIST 视口外泄漏 =================
|
||||
* 把页面 scroll 拉到底,列表上屏;再把列表 scroll 设非整行倍数 + 选中一个跨底/跨顶行,
|
||||
* 观察视口外(标题间隙/底缘下)是否漏出选中行填充(标脏泄漏 = 标题栏/底部卡脏区)。 */
|
||||
demo_rebuild_to(100000); /* clamp 到 max,列表上屏 */
|
||||
printf("P3 list rect = (%d,%d,%d,%d)\n", r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
/* 顶部泄漏:选中行0 + scroll=20 → 行0 ry=top-20,填充越顶泄漏 */
|
||||
meow_list_set_selected(g_dlc, 0); meow_list_set_scroll(g_dlc, 20);
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
int ux = r_list.x + 10, uy = r_list.y - 5; /* 列表顶上方 5px(标题间隙) */
|
||||
int ur, ug, ub; rd_px(ux, uy, &ur, &ug, &ub);
|
||||
printf("P3 top-leak "); pr_px("above-top", ux, uy);
|
||||
if (is_bg(ur, ug, ub)) printf(" P3-top-PASS no leak into title gap\n");
|
||||
else { printf(" P3-top-FAIL leaked into title gap\n"); fails++; }
|
||||
/* 底部泄漏:选中行4 + scroll=20 → 行4 ry=590..630 越底泄漏 */
|
||||
meow_list_set_selected(g_dlc, 4); meow_list_set_scroll(g_dlc, 20);
|
||||
meow_ui_invalidate(r_list.x, r_list.y, r_list.w, r_list.h);
|
||||
int dx = r_list.x + 10, dy_ = r_list.y + r_list.h + 5; /* 列表底下方 5px */
|
||||
int dr, dg, db; rd_px(dx, dy_, &dr, &dg, &db);
|
||||
printf("P3 bot-leak "); pr_px("below-bottom", dx, dy_);
|
||||
if (is_bg(dr, dg, db)) printf(" P3-bottom-PASS no leak below list\n");
|
||||
else { printf(" P3-bottom-FAIL leaked below list\n"); fails++; }
|
||||
|
||||
printf(fails ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", fails);
|
||||
return fails;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,125 @@
|
||||
/* test_pinyin.c — 中文拼音 IME 宿主验证(480x720 离屏,直接 handle_tap 确定语义)。
|
||||
* ① 拼音组合:type "nihao" → 组合串="nihao"、字段=""(组合不上屏、on_change 不触发);
|
||||
* 点候选(你好)→ 字段="你好"、组合清空、on_change="你好";
|
||||
* ② 候选翻页:type "shi" → nc>6;点 ▶ 翻页末首候选变化、点 ◀ 回;
|
||||
* ③ UTF-8 删字段:上屏"你好"后点退格 → 字段="你"(按 codepoint 回退,不出现乱码);
|
||||
* ④ 中/英切换:拼音点 [拼/英] → ASCII;ASCII 点顶条「中」热区 → 拼音;
|
||||
* ⑤ 空格/回车上屏首候选:type "ni" + 空格 → 字段="你"。
|
||||
*
|
||||
* 编译(宿主 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
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_pinyin.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
/* 拼音键位(build_kb PINYIN:行 y=504/552/600/648,GCOL=48,行左缘 x=2;键 44×36):
|
||||
* row1 y=504 "qwertyuiop";row2 y=552 "asdfghjkl"+[拼/英]@col9;
|
||||
* row3 y=600 "zxcvbnm"+[←退格]@col7;row4 y=648 [?123]+[空格]+[OK]。 */
|
||||
#define KEY_XY(col, y) (((2 + (col) * 48) + 22), ((y) + 18))
|
||||
static void t(char ch) {
|
||||
/* 按字符找其键中心坐标(行/列换算) */
|
||||
int row = 0, col = 0;
|
||||
const char *rows[3] = { "qwertyuiop", "asdfghjkl", "zxcvbnm" };
|
||||
for (int r = 0; r < 3; r++) {
|
||||
const char *p = strchr(rows[r], ch);
|
||||
if (p) { row = r; col = (int)(p - rows[r]); break; }
|
||||
}
|
||||
int x = 2 + col * 48 + 22;
|
||||
int y = 504 + row * 48 + 18;
|
||||
meow_ime_handle_tap(x, y);
|
||||
}
|
||||
|
||||
static char g_echo[128] = "";
|
||||
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
|
||||
static void tap(int x, int y) { meow_ime_handle_tap(x, y); }
|
||||
|
||||
static meow_ime_ctx g_c;
|
||||
static void open_py(void) {
|
||||
memset(&g_c, 0, sizeof g_c);
|
||||
g_c.mode = MEOW_IME_PINYIN; g_c.maxlen = 64; g_c.on_change = ime_cb;
|
||||
memset(g_echo, 0, sizeof g_echo);
|
||||
meow_ui_text_input_async(&g_c);
|
||||
assert(meow_ime_active() == 1);
|
||||
}
|
||||
static void close_py(void) { meow_ime_close(0); assert(meow_ime_active() == 0); }
|
||||
|
||||
int main(void) {
|
||||
setenv("MEOW_UI_DUMP", "/tmp/pinyin.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
|
||||
/* ① 拼音组合 + 点上屏 */
|
||||
open_py();
|
||||
t('n'); t('i'); t('h'); t('a'); t('o');
|
||||
assert(strcmp(meow_ime_composition(), "nihao") == 0);
|
||||
assert(meow_ime_text()[0] == 0); /* 组合不上屏 */
|
||||
assert(g_echo[0] == 0); /* 未上屏不回调 */
|
||||
/* 点候选 0(你好,x=44 起;宿主 stub width=strlen*6) */
|
||||
tap(60, 480);
|
||||
assert(strcmp(meow_ime_text(), "你好") == 0);
|
||||
assert(strcmp(g_echo, "你好") == 0);
|
||||
assert(meow_ime_composition()[0] == 0); /* 组合已清 */
|
||||
close_py();
|
||||
printf("PASS pinyin: nihao -> 你好 (compose+commit+on_change)\n"); fflush(stdout);
|
||||
|
||||
/* ② 候选翻页(shi 候选 >6) */
|
||||
open_py();
|
||||
t('s'); t('h'); t('i');
|
||||
char all[32][64];
|
||||
int n = py_predict("shi", all, 32);
|
||||
assert(n > 6);
|
||||
assert(strcmp(all[0], "是") == 0);
|
||||
/* 点 ▶(下一页)→ 页内首候选应为 all[6](页 0 首候选是 all[0]) */
|
||||
tap(457, 480); /* ▶:页 0 → 页 1 */
|
||||
tap(60, 480); /* 点页 1 候选 0(x=44 起) */
|
||||
assert(strcmp(meow_ime_text(), all[6]) == 0);
|
||||
close_py();
|
||||
printf("PASS pagination: shi page_next then commit page1[0] == all[6]\n"); fflush(stdout);
|
||||
|
||||
/* ③ UTF-8 删字段:上屏"你好"后点退格 → "你" */
|
||||
open_py();
|
||||
t('n'); t('i'); t('h'); t('a'); t('o');
|
||||
tap(60, 480); /* 上屏"你好" */
|
||||
assert(strcmp(meow_ime_text(), "你好") == 0);
|
||||
tap(410, 618); /* ←退格(row3 col7 宽键中心) */
|
||||
assert(strcmp(meow_ime_text(), "你") == 0);
|
||||
assert(strcmp(g_echo, "你") == 0);
|
||||
close_py();
|
||||
printf("PASS utf8 field backspace: 你好 -> 你\n"); fflush(stdout);
|
||||
|
||||
/* ④ 中/英切换 */
|
||||
open_py();
|
||||
tap(456, 570); /* [拼/英] row2 col9 → ASCII */
|
||||
assert(meow_ime_active() == 1);
|
||||
/* 顶条「中」热区(430..474, KB_TOP..KB_TOP+42)→ 切回拼音;组合已被丢弃 */
|
||||
tap(452, 438);
|
||||
assert(meow_ime_active() == 1);
|
||||
t('n'); t('i');
|
||||
assert(strcmp(meow_ime_composition(), "ni") == 0);
|
||||
tap(456, 570); /* 又回 ASCII */
|
||||
assert(strcmp(meow_ime_composition(), "") == 0); /* 组合态已丢弃 */
|
||||
close_py();
|
||||
printf("PASS zh/en switch (pinyin->ascii->pinyin, combo dropped)\n"); fflush(stdout);
|
||||
|
||||
/* ⑤ 空格上屏首候选(ni → 你) */
|
||||
open_py();
|
||||
t('n'); t('i');
|
||||
tap(194, 666); /* 空格 */
|
||||
assert(strcmp(meow_ime_text(), "你") == 0);
|
||||
assert(strcmp(g_echo, "你") == 0);
|
||||
close_py();
|
||||
printf("PASS space commits first candidate: ni -> 你\n"); fflush(stdout);
|
||||
|
||||
printf("ALL PASS\n");
|
||||
meow_ui_shutdown();
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,124 @@
|
||||
/* test_sb_collapse.c — 宿主验证「全屏状态栏收合」纯逻辑(agent-146,不依赖 /dev/fb0/字库):
|
||||
* 1. 顶条模式上报:App 进程(system_mode=0)set_collapsed/set_fullscreen → 写 /tmp/meowapp_sbar
|
||||
* ('0'展开/'1'收合/'2'隐藏),供 launcher 感知 App 运行时全屏/收合态。
|
||||
* 2. 热区保留判定:状态栏激活(含收合)时 meow_ui_statusbar_hotzone()==MUI_SBAR_H(28)(收合≠禁热区,
|
||||
* 渲染与热区事件分离);仅全屏隐藏(inset0)时缩到 MEOW_SBAR_HOTZONE_FS(4)。
|
||||
* 3. 收合状态机:system_mode=1 + animate(200) + set_collapsed(1) → animating()==1,
|
||||
* collapse_offset() 随时间推进 → 28(收合出屏);set_collapsed(0) → offset 回 0(恢复)。
|
||||
* 4. 收合 inset:collapsed → meow_ui_statusbar_height()==MUI_SBAR_COLLAPSE_H(4)。
|
||||
*
|
||||
* 编译(宿主 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
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <assert.h>
|
||||
|
||||
#define SBAR_MODE_FILE "/tmp/meowapp_sbar"
|
||||
|
||||
static int read_mode(void)
|
||||
{
|
||||
int fd = open(SBAR_MODE_FILE, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
char ch[8]; int n = read(fd, ch, sizeof ch - 1);
|
||||
close(fd);
|
||||
if (n <= 0) return -1;
|
||||
return ch[0] == '1' ? 1 : (ch[0] == '2' ? 2 : 0);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
/* 初始 App 模式(system_mode 默认 0) */
|
||||
assert(meow_ui_sys_render_enabled() == 0);
|
||||
|
||||
/* ---- 1) 顶条模式上报 ---- */
|
||||
meow_ui_set_fullscreen(0); meow_ui_statusbar_set_collapsed(0);
|
||||
assert(read_mode() == 0); /* 展开 */
|
||||
meow_ui_statusbar_set_collapsed(1);
|
||||
assert(read_mode() == 1); /* 收合 */
|
||||
meow_ui_statusbar_set_collapsed(0);
|
||||
meow_ui_set_fullscreen(1);
|
||||
assert(read_mode() == 2); /* 隐藏 */
|
||||
meow_ui_set_fullscreen(0);
|
||||
printf("PASS mode-file: app writes /tmp/meowapp_sbar (0=expanded 1=collapsed 2=hidden)\n");
|
||||
|
||||
/* ---- 2a) 激活(含收合)时热区保留 28 ---- */
|
||||
meow_ui_statusbar_set_collapsed(1);
|
||||
assert(meow_ui_statusbar_collapsed() == 1);
|
||||
assert(meow_ui_statusbar_hotzone() == MUI_SBAR_H); /* 收合仍监听顶部 28px(渲染与热区分离) */
|
||||
assert(meow_ui_statusbar_height() == MUI_SBAR_COLLAPSE_H); /* 收合 inset=4(提示线) */
|
||||
assert(meow_ui_sys_statusbar_active() == 1); /* 收合仍视为状态栏激活 */
|
||||
printf("PASS hotzone-retain: collapsed -> hotzone=%d(28) inset=%d(4) active\n",
|
||||
meow_ui_statusbar_hotzone(), meow_ui_statusbar_height());
|
||||
|
||||
/* ---- 2b) 全屏隐藏(immersive)时热区缩小 ---- */
|
||||
meow_ui_statusbar_set_collapsed(0);
|
||||
meow_ui_set_fullscreen(1);
|
||||
int fs_hot = meow_ui_statusbar_hotzone();
|
||||
assert(fs_hot == MEOW_SBAR_HOTZONE_FS); /* inset0 → 4 */
|
||||
printf("PASS hotzone-shrink: fullscreen-hidden -> hotzone=%d(fs=4)\n", fs_hot);
|
||||
meow_ui_set_fullscreen(0);
|
||||
meow_ui_statusbar_set_collapsed(0);
|
||||
|
||||
/* ---- 3) 收合状态机(launcher / system_mode=1)---- */
|
||||
meow_ui_sys_set_system_mode(1);
|
||||
assert(meow_ui_sys_render_enabled() == 1);
|
||||
meow_ui_set_refresh_always_max(1); /* 宿主:绕过帧率门控,逐次 flush 可推进 */
|
||||
|
||||
meow_ui_statusbar_set_collapsed(0); /* 复位 */
|
||||
meow_ui_set_fullscreen(0);
|
||||
meow_ui_statusbar_animate(200); /* 上移过渡 200ms */
|
||||
assert(meow_ui_statusbar_animating() == 0);
|
||||
meow_ui_statusbar_set_collapsed(1); /* 目标收合 */
|
||||
assert(meow_ui_statusbar_animating() == 1); /* 未到位 → 过渡中 */
|
||||
assert(meow_ui_statusbar_collapse_offset() == 0);
|
||||
|
||||
/* 驱动 present(标顶条左上 1px 唤醒 + 上屏)并让时间流逝,验证 offset 推进 */
|
||||
int off_prev = 0, progressed = 0;
|
||||
for (int i = 0; i < 60; i++) {
|
||||
meow_ui_flush_region(0, 0, 1, 1);
|
||||
usleep(15000); /* ~15ms/帧,模拟过渡期高频刷 */
|
||||
int off = meow_ui_statusbar_collapse_offset();
|
||||
if (off > off_prev) progressed = 1;
|
||||
off_prev = off;
|
||||
if (!meow_ui_statusbar_animating()) break; /* 到 28 稳定 */
|
||||
}
|
||||
assert(progressed == 1);
|
||||
assert(meow_ui_statusbar_collapse_offset() >= MUI_SBAR_H);
|
||||
assert(meow_ui_statusbar_animating() == 0);
|
||||
assert(meow_ui_statusbar_height() == MUI_SBAR_COLLAPSE_H); /* 收合稳定:inset=4 */
|
||||
printf("PASS collapse-enter: offset 0->%d (animating 1->0)\n",
|
||||
meow_ui_statusbar_collapse_offset());
|
||||
|
||||
/* 退出全屏(恢复):offset 回 0 */
|
||||
meow_ui_statusbar_set_collapsed(0);
|
||||
assert(meow_ui_statusbar_animating() == 1);
|
||||
int back = 0;
|
||||
for (int i = 0; i < 60; i++) {
|
||||
meow_ui_flush_region(0, 0, 1, 1);
|
||||
usleep(15000);
|
||||
back = meow_ui_statusbar_collapse_offset();
|
||||
if (!meow_ui_statusbar_animating()) break;
|
||||
}
|
||||
assert(back == 0 && meow_ui_statusbar_animating() == 0);
|
||||
assert(meow_ui_statusbar_height() == MUI_SBAR_H); /* 恢复:inset=28 */
|
||||
printf("PASS collapse-exit: offset ->0 (inset back to 28)\n");
|
||||
|
||||
/* 清理:宿主测试产物 */
|
||||
unlink(SBAR_MODE_FILE);
|
||||
|
||||
printf("ALL PASS\n");
|
||||
meow_ui_shutdown();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/* test_sb_statusbar.c — 宿主交叉验证「同一个状态栏」闭环的纯逻辑(不依赖 /dev/fb0/字库):
|
||||
* 1. 数据源唯一:launcher 经 meow_ui_sys_feed 喂权威快照 → meow_ui_sys_get() 反映一致
|
||||
* (batt/vol/wifi_connected/wifi_level/bt/brightness)。
|
||||
* 2. 系统模式门:launcher 置 system_mode=1 才渲染(meow_ui_sys_statusbar_render 画图标+值);
|
||||
* App 进程默认 0 → render 为 no-op(不画顶条、无双画)。
|
||||
* 3. 图标簇+值:fed 数据 → render 依次调用 meow_gfx_blit_icon(电量/音量/蓝牙/WiFi/亮度)
|
||||
* 与 meow_ui_text_right(%n 值);WiFi 已连-50dBm → WIFI_STRONG。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_sb_statusbar test_sb_statusbar.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c \
|
||||
* -I. -I../meow_launcherd
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "meow_ui_sys.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <assert.h>
|
||||
|
||||
/* ---- gfx/text/icon/log 打桩(宿主无 fb/字库) ---- */
|
||||
static int g_calls_icon[16]; /* meow_gfx_blit_icon 的 idx 调用计数 */
|
||||
static int g_calls_icon_total = 0;
|
||||
static char g_last_text_right[128]; /* 最近一次 meow_ui_text_right 的串 */
|
||||
static int g_fill_top = 0; /* 清顶条(0,0,w,28)调用计数 */
|
||||
static int g_render_calls = 0; /* statusbar_render 里真正执行了的次数(用 fill 计数近似) */
|
||||
static int g_collapsed = 0;
|
||||
|
||||
int meow_ui_width(void) { return 480; }
|
||||
int meow_ui_text_height(int size) { (void)size; return 20; }
|
||||
int meow_ui_text_width(const char *s, int size) { (void)size; return (int)strlen(s) * 7; }
|
||||
void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t c) {
|
||||
(void)c; if (x == 0 && y == 0 && w == 480 && h == 28) g_fill_top++;
|
||||
}
|
||||
void meow_ui_text(int x, int y, const char *s, int size, uint32_t c) { (void)x;(void)y;(void)s;(void)size;(void)c; }
|
||||
void meow_ui_text_right(int rx, int y, const char *s, int size, uint32_t c) {
|
||||
(void)rx;(void)y;(void)size;(void)c;
|
||||
snprintf(g_last_text_right, sizeof g_last_text_right, "%s", s);
|
||||
}
|
||||
void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) {
|
||||
(void)x;(void)y;(void)ink_h;(void)fg;(void)cy;
|
||||
if (idx >= 0 && idx < 16) g_calls_icon[idx]++;
|
||||
g_calls_icon_total++;
|
||||
}
|
||||
int meow_ui_statusbar_collapsed(void) { return g_collapsed; }
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl;(void)fmt; }
|
||||
|
||||
/* ---- feed 权威快照(模拟 launcher sb_sdk_refresh / app_overlay_statusbar) ---- */
|
||||
static void feed(const meow_ui_feed *f) { meow_ui_sys_feed(f); }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* init + 系统模式(launcher 进程) */
|
||||
meow_ui_sys_init();
|
||||
assert(meow_ui_sys_render_enabled() == 0); /* 默认 App 模式 */
|
||||
|
||||
/* 1) 数据源唯一:喂入 → sys_get 一致 */
|
||||
meow_ui_feed f;
|
||||
memset(&f, 0, sizeof f);
|
||||
f.batt = 81; f.charging = 1; f.wifi_on = 1; f.wifi_connected = 1; f.wifi_level = -50;
|
||||
f.bt_seen = 1; f.bt_on = 1; f.vol = 60; f.brightness = 100;
|
||||
feed(&f);
|
||||
const meow_sys_state *s = meow_ui_sys_get();
|
||||
assert(s->batt == 81 && s->charging == 1);
|
||||
assert(s->wifi_on == 1 && s->wifi_connected == 1 && s->wifi_level == -50);
|
||||
assert(s->bt_seen == 1 && s->bt_on == 1);
|
||||
assert(s->vol == 60 && s->brightness == 100);
|
||||
printf("PASS data-source unique: feed -> sys_get consistent (batt=%d vol=%d wifi_lvl=%d bri=%d)\n",
|
||||
s->batt, s->vol, s->wifi_level, s->brightness);
|
||||
|
||||
/* 2a) App 进程(system_mode=0):render 应 no-op(无图标 blit、无顶条 fill),
|
||||
* 但 inset 仍为 28px(App 内容下移让位给 launcher 叠画的状态栏)——即「有 inset、
|
||||
* 无状态栏实例」,根治双画。 */
|
||||
memset(g_calls_icon, 0, sizeof g_calls_icon); g_calls_icon_total = 0; g_fill_top = 0;
|
||||
meow_ui_set_statusbar(1);
|
||||
assert(meow_ui_statusbar_height() == MUI_SBAR_H); /* App 仍 inset 28 */
|
||||
assert(meow_ui_statusbar_visible() == 1);
|
||||
meow_ui_sys_statusbar_render();
|
||||
assert(g_calls_icon_total == 0 && g_fill_top == 0);
|
||||
printf("PASS app-mode gate: render no-op (no icon/fill) + inset=28 -> no double draw\n");
|
||||
|
||||
/* 2b) 系统进程(system_mode=1):render 画图标+值 */
|
||||
meow_ui_sys_set_system_mode(1);
|
||||
assert(meow_ui_sys_render_enabled() == 1);
|
||||
memset(g_calls_icon, 0, sizeof g_calls_icon); g_calls_icon_total = 0; g_fill_top = 0;
|
||||
g_last_text_right[0] = 0;
|
||||
meow_ui_sys_statusbar_render();
|
||||
assert(g_fill_top >= 1); /* 清整条顶栏 */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_BATT] >= 1); /* 电量图标 */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_VOL] >= 1); /* 音量图标 */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_BT_CONN] >= 1);/* 蓝牙连接图标 */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_WIFI_STRONG] >= 1); /* WiFi 已连-50dBm → 强 */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_BRIGHT] >= 1);/* 亮度图标 */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_WIFI_STRONG] >= 1); /* 已连 → 强(正确分档) */
|
||||
assert(g_calls_icon[MEOW_SB_ICON_WIFI_WEAK] == 0);
|
||||
printf("PASS icon cluster: batt/vol/bt/wifi(strong)/bright blitted, values=%s\n",
|
||||
g_last_text_right);
|
||||
|
||||
/* 3) WiFi 弱信号(-80dBm)→ WEAK;关网卡 → 不画 WiFi */
|
||||
memset(g_calls_icon, 0, sizeof g_calls_icon);
|
||||
f.wifi_connected = 1; f.wifi_level = -80; feed(&f);
|
||||
meow_ui_sys_statusbar_render();
|
||||
assert(g_calls_icon[MEOW_SB_ICON_WIFI_WEAK] >= 1);
|
||||
memset(g_calls_icon, 0, sizeof g_calls_icon);
|
||||
f.wifi_on = 0; f.wifi_connected = 0; f.wifi_level = -1000; feed(&f);
|
||||
meow_ui_sys_statusbar_render();
|
||||
assert(g_calls_icon[MEOW_SB_ICON_WIFI_STRONG] == 0 && g_calls_icon[MEOW_SB_ICON_WIFI_WEAK] == 0);
|
||||
printf("PASS wifi tiering: weak(-80)->WEAK, off->no wifi icon\n");
|
||||
|
||||
printf("ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/* test_sdk_fixes.c — M2.71 修复项宿主验证(离屏 480x720,16bpp)。
|
||||
* 覆盖:
|
||||
* bug4 disabled 按钮圆角外无灰方块(角点 = 页面背景而非叠暗);
|
||||
* bug1 圆形进度环带确实画到 = 半径+thk/2 处(bbox 与真实像素一致,漏刷修复的几何前提);
|
||||
* bug3 modal(Dialog)open→cancel 后背景还原(scrim 不累积/无残影);
|
||||
* bug7 IME 回显实时同步(on_change 随键入触发)+ 关闭后键盘区背景还原;
|
||||
* fix1 dialog INPUT 在 ESC/背景(result=0)关闭后文本不丢(M2.71-IME-fix 新语义:回车只提交不关)。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
|
||||
* gcc -Wall -o test_sdk_fixes test_sdk_fixes.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.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();
|
||||
assert(bb && line > 0);
|
||||
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 near(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 12; }
|
||||
|
||||
/* ---- 为阻塞模态/IME 在后台注入事件 ---- */
|
||||
static int g_inj_tap_x = 0, g_inj_tap_y = 0, g_inj_delay = 0;
|
||||
static int K_DLG_OK_X = 0, K_DLG_OK_Y = 0; /* dialog INPUT 的 OK 按钮中心(运行时算) */
|
||||
static void *inject_thread(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
usleep((useconds_t)g_inj_delay);
|
||||
meow_input_inject(MEOW_EV_TAP, g_inj_tap_x, g_inj_tap_y);
|
||||
return NULL;
|
||||
}
|
||||
static void inject_after(int ms, int x, int y)
|
||||
{
|
||||
pthread_t th; g_inj_tap_x = x; g_inj_tap_y = y; g_inj_delay = ms;
|
||||
pthread_create(&th, NULL, inject_thread, NULL);
|
||||
pthread_detach(th);
|
||||
}
|
||||
|
||||
static void test_disabled_corner(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
meow_widget *b = meow_btn_create(100, 100, 200, 60, "DISABLED");
|
||||
meow_widget_set_enabled(b, 0); /* 禁用:old 用方形 α 盖整 bbox → 圆角外露灰 */
|
||||
meow_widget_draw(b);
|
||||
meow_ui_flush();
|
||||
int r, g, bb;
|
||||
rd_px(100, 100, &r, &g, &bb); /* bbox 左上角(圆角外):应为页面背景 */
|
||||
printf("[bug4] disabled corner rgb=%02X%02X%02X (bg=%02X%02X%02X) ",
|
||||
r, g, bb, (th->bg >> 16) & 0xFF, (th->bg >> 8) & 0xFF, th->bg & 0xFF);
|
||||
assert(near(r, (th->bg >> 16) & 0xFF) && near(g, (th->bg >> 8) & 0xFF) && near(bb, th->bg & 0xFF));
|
||||
printf("PASS\n");
|
||||
meow_widget_free(b);
|
||||
}
|
||||
|
||||
static void test_circular_extent(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
/* r=30, thk=4 → 环外缘 = r+thk/2 = 32;create 几何外接框 = 中心±(r+margin)=±33 */
|
||||
int cx = 240, cy = 200, r = 30;
|
||||
meow_widget *pg = meow_progress_circular_create(cx, cy, r, 100);
|
||||
meow_widget_draw(pg);
|
||||
meow_ui_flush();
|
||||
int rd, g, b;
|
||||
rd_px(cx - 32, cy, &rd, &g, &b); /* 最左环外缘:应被画(非背景) */
|
||||
int bg_r = (th->bg >> 16) & 0xFF, bg_g = (th->bg >> 8) & 0xFF, bg_b = th->bg & 0xFF;
|
||||
printf("[bug1] ring-left-extent rgb=%02X%02X%02X bg=%02X%02X%02X ", rd, g, b, bg_r, bg_g, bg_b);
|
||||
assert(!(near(rd, bg_r) && near(g, bg_g) && near(b, bg_b))); /* 环带已画到外缘 */
|
||||
printf("PASS\n"); fflush(stdout);
|
||||
meow_widget_free(pg);
|
||||
}
|
||||
|
||||
static void test_modal_restore(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
meow_ui_flush();
|
||||
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "Sure?");
|
||||
/* 两次 open→cancel:背景应两次均还原(scrim 不累积、无残影) */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
inject_after(30, 1, 1); /* (1,1) 在居中卡片外 → 取消 */
|
||||
meow_dialog_show(d);
|
||||
int r, g, b;
|
||||
rd_px(400, 100, &r, &g, &b); /* 卡片外右上:应 == 背景 */
|
||||
printf("[bug3] open%d cancel bg rgb=%02X%02X%02X bg=%02X%02X%02X ", i, r, g, b,
|
||||
(th->bg >> 16) & 0xFF, (th->bg >> 8) & 0xFF, th->bg & 0xFF);
|
||||
assert(near(r, (th->bg >> 16) & 0xFF) && near(g, (th->bg >> 8) & 0xFF) && near(b, th->bg & 0xFF));
|
||||
}
|
||||
printf("PASS\n");
|
||||
meow_dialog_free(d);
|
||||
}
|
||||
|
||||
/* IME 回显同步回调 */
|
||||
static char g_echo[64] = "";
|
||||
static void ime_cb(const char *t, void *ctx) { (void)ctx; snprintf(g_echo, sizeof g_echo, "%s", t); }
|
||||
|
||||
static void test_ime_echo_and_restore(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
meow_ui_flush();
|
||||
meow_ime_ctx c; memset(&c, 0, sizeof c);
|
||||
c.mode = MEOW_IME_ASCII; c.maxlen = 32; c.on_change = ime_cb;
|
||||
/* --- 异步:验『回显/输入框实时同步(on_change 随键入触发)』--- */
|
||||
meow_ui_text_input_async(&c);
|
||||
assert(meow_ime_active() == 1);
|
||||
meow_input_inject(MEOW_EV_TAP, 20, 580); /* ASCII 行3第1键 = 'a'(shift 关 → 小写 'a') */
|
||||
meow_ime_handle_tap(20, 580);
|
||||
assert(strcmp(g_echo, "a") == 0 && strcmp(meow_ime_text(), "a") == 0);
|
||||
printf("[bug7] ime echo live-sync after 'a' -> \"%s\" (shift-off lower) PASS\n", g_echo); fflush(stdout);
|
||||
meow_ime_handle_tap(450, 584); /* SHIFT 开 */
|
||||
meow_ime_handle_tap(20, 580); /* 'a' → 'A' 大写 */
|
||||
assert(strcmp(g_echo, "aA") == 0 && strcmp(meow_ime_text(), "aA") == 0);
|
||||
printf("[bug7] shift-on -> 'A' (echo \"%s\") PASS\n", g_echo); fflush(stdout);
|
||||
meow_input_inject(MEOW_EV_TAP, 20, 680); /* ESC(X) → 取消 */
|
||||
meow_ime_handle_tap(20, 680);
|
||||
assert(meow_ime_active() == 0);
|
||||
/* --- 阻塞:验『关闭后键盘区背景还原』--- */
|
||||
meow_ui_clear(th->bg);
|
||||
meow_ui_flush();
|
||||
inject_after(30, 20, 680); /* ESC 关闭 */
|
||||
int rc = meow_ui_text_input(&c);
|
||||
assert(rc == 0); /* 取消 */
|
||||
int r, g, b;
|
||||
rd_px(240, 600, &r, &g, &b); /* 键盘区内一点:应 == 背景 */
|
||||
printf("[bug7] ime close restore px rgb=%02X%02X%02X bg=%02X%02X%02X ", r, g, b,
|
||||
(th->bg >> 16) & 0xFF, (th->bg >> 8) & 0xFF, th->bg & 0xFF);
|
||||
assert(near(r, (th->bg >> 16) & 0xFF) && near(g, (th->bg >> 8) & 0xFF) && near(b, th->bg & 0xFF));
|
||||
printf("PASS\n");
|
||||
}
|
||||
|
||||
/* ---- 回归①:dialog INPUT 在 ESC/背景(result=0)关闭后文本不丢(M2.71-IME-fix 新语义)。
|
||||
* 复现 dialog.c dlg_input_ok_center → 复用 dialog 的 OK 按钮中心几何(与 dlg_calc 同一公式)。 */
|
||||
static void dialog_input_ok_center(int *ox, int *oy)
|
||||
{
|
||||
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, btn = 44;
|
||||
int tH = meow_ui_text_height(th->font_title);
|
||||
int bH = meow_ui_text_height(th->font_body);
|
||||
int content = bH + 8; /* INPUT(无 items)→ 1 行正文预览 */
|
||||
int dw = 348; if (dw > W - 40) dw = W - 40;
|
||||
int dx = (W - dw) / 2;
|
||||
int dh = pad + tH + 12 + content + pad + (16 + btn);
|
||||
int avail = H - sb - 20;
|
||||
if (dh > avail) dh = avail;
|
||||
int dy = (H - dh) / 2; if (dy < sb) dy = sb;
|
||||
int row_y = dy + pad + tH + 12;
|
||||
int btn_y = row_y + content + 16;
|
||||
int btn_w = (dw - 2 * pad - 12) / 2;
|
||||
int cx = dx + pad + btn_w + 12; /* OK = 右侧填充按钮左缘 */
|
||||
*ox = cx + btn_w / 2;
|
||||
*oy = btn_y + btn / 2;
|
||||
}
|
||||
|
||||
static void *dlg_input_driver(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
usleep(40000); /* 等 dialog show 起(遮罩+卡片已绘) */
|
||||
meow_input_inject(MEOW_EV_TAP, K_DLG_OK_X, K_DLG_OK_Y); /* 点 OK → 打开阻塞 IME */
|
||||
usleep(140000);
|
||||
meow_input_inject(MEOW_EV_TAP, 20, 584); /* 行3第1键 'a'(shift 关) */
|
||||
usleep(140000);
|
||||
meow_input_inject(MEOW_EV_TAP, 20, 680); /* ESC → 关 IME → dialog 随之关 */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_dialog_input_preserve(void)
|
||||
{
|
||||
const meow_theme *th = meow_theme_get();
|
||||
meow_ui_clear(th->bg);
|
||||
meow_ui_flush();
|
||||
meow_ime_ctx ime; memset(&ime, 0, sizeof ime);
|
||||
ime.mode = MEOW_IME_ASCII; ime.maxlen = 64;
|
||||
strcpy(ime.buf, "");
|
||||
meow_dialog *d = meow_dialog_create(MEOW_DLG_INPUT, "Name");
|
||||
meow_dialog_set_ime(d, &ime);
|
||||
|
||||
dialog_input_ok_center(&K_DLG_OK_X, &K_DLG_OK_Y);
|
||||
pthread_t thd; pthread_create(&thd, NULL, dlg_input_driver, NULL);
|
||||
meow_dialog_show(d);
|
||||
pthread_join(thd, NULL);
|
||||
|
||||
/* ESC 关闭(result=0)→ 文本应保留:App 原始 ctx.buf 应含 'a'(不再丢失),且是取消语义 */
|
||||
printf("[fix1] dialog INPUT after ESC: result=%d App.ime.buf=\"%s\" (expect 0, \"a\")\n",
|
||||
meow_dialog_result(d), ime.buf);
|
||||
assert(meow_dialog_result(d) == 0);
|
||||
assert(strcmp(ime.buf, "a") == 0);
|
||||
printf("[fix1] dialog INPUT text preserved on ESC PASS\n");
|
||||
meow_dialog_free(d);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_UI_DUMP", "/tmp/fixes.ppm", 1);
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
assert(meow_ui_init(&o) == 0);
|
||||
test_disabled_corner();
|
||||
test_circular_extent();
|
||||
test_modal_restore();
|
||||
test_ime_echo_and_restore();
|
||||
test_dialog_input_preserve();
|
||||
meow_ui_shutdown();
|
||||
printf("ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* test_sdk_input_keys.c — M2.55 宿主验证:SDK 输入层接管系统按键语义(App 前台也生效)。
|
||||
*
|
||||
* 覆盖(直接调输入层语义函数,等效"模拟按键事件"):
|
||||
* K1 音量 +/step:audio.volume 迈一步(volume_step 约束),settings.conf 落到值改变;
|
||||
* K2 音量 ± clamp:volume_max 软上限、0 下限、硬上限 100;到限不再写;
|
||||
* K3 保留其它键:改 audio.volume 不整文件 O_TRUNC(display.brightness/注释保留);
|
||||
* K4 媒体键本地回调:注册 playctl 回调后,sys_key(上一曲/下一曲/播放暂停) → 回调 cmd 正确
|
||||
* (播放/暂停恒发 toggle(6),SDK 不再本地判 state——走自有 IPC 线程的源不经 SDK meta
|
||||
* 上报,本地 state 恒 0 会恒发「播放」致播放中按键无效;源侧按当前态翻转);
|
||||
* K5 媒体键 IPC 兜底:未注册回调(g_cb==NULL)→ 走 IPC 发 launcher(无 socket → 静默不崩)。
|
||||
*
|
||||
* 编译(宿主 gcc,仅链 vol/media 两个 TU;无需 meow_ui 其它子系统):
|
||||
* gcc -Wall -Wextra -pthread -o test_sdk_input_keys test_sdk_input_keys.c \
|
||||
* meow_ui_vol.c meow_ui_media.c -I. -I../meow_launcherd
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
|
||||
static int failures = 0;
|
||||
static void check(const char *name, int cond)
|
||||
{
|
||||
printf("%s %s\n", cond ? "PASS" : "FAIL", name);
|
||||
if (!cond) failures++;
|
||||
}
|
||||
|
||||
static char g_path[64];
|
||||
static void wfile(const char *content)
|
||||
{
|
||||
FILE *f = fopen(g_path, "w");
|
||||
if (!f) { perror("fopen"); exit(2); }
|
||||
fputs(content, f);
|
||||
fclose(f);
|
||||
}
|
||||
static int vol_in_file(void)
|
||||
{
|
||||
return meow_ui_vol_read(g_path, "audio.volume", -1);
|
||||
}
|
||||
|
||||
/* —— 媒体回调捕获 —— */
|
||||
static int cb_calls = 0, cb_cmd = 0;
|
||||
static int64_t cb_seek = 0;
|
||||
static void playctl_cb(int cmd, int64_t seek_ms)
|
||||
{ cb_calls++; cb_cmd = cmd; cb_seek = seek_ms; }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
snprintf(g_path, sizeof g_path, "/tmp/meow_kb_%d.conf", (int)getpid());
|
||||
char sock[64]; /* 指向不存在路径:让 IPC 兜底路径确定性静默失败 */
|
||||
snprintf(sock, sizeof sock, "/tmp/meow_kb_%d.sock", (int)getpid());
|
||||
setenv("MEOW_IPC_SOCK", sock, 1);
|
||||
|
||||
/* ---- K1 音量迈一步(volume_step=1 默认)---- */
|
||||
wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 1\n");
|
||||
check("K1 vol 60 +1 -> 61", meow_ui_vol_step(g_path, +1) == 61);
|
||||
check("K1 settings.conf 落到 61", vol_in_file() == 61);
|
||||
check("K1 vol 61 -1 -> 60", meow_ui_vol_step(g_path, -1) == 60);
|
||||
check("K1 settings.conf 回到 60", vol_in_file() == 60);
|
||||
|
||||
/* ---- K2 步进 volume_step=4 + clamp ---- */
|
||||
wfile("audio.volume = 60\naudio.volume_max = 100\naudio.volume_step = 4\n");
|
||||
check("K2 step=4: 60 +1 -> 64", meow_ui_vol_step(g_path, +1) == 64);
|
||||
wfile("audio.volume = 99\naudio.volume_max = 100\naudio.volume_step = 4\n");
|
||||
check("K2 step=4: 99 +1 -> 100 (硬上限)", meow_ui_vol_step(g_path, +1) == 100);
|
||||
|
||||
/* volume_max 软上限 */
|
||||
wfile("audio.volume = 60\naudio.volume_max = 55\naudio.volume_step = 1\n");
|
||||
meow_ui_vol_step(g_path, +1);
|
||||
check("K2 volume_max=55: vol -> 55 (clamp 上限)", vol_in_file() == 55);
|
||||
|
||||
/* 到上限不再写 */
|
||||
wfile("audio.volume = 100\naudio.volume_max = 100\naudio.volume_step = 1\n");
|
||||
check("K2 at hard max: +1 -> 100 (no write)", meow_ui_vol_step(g_path, +1) == 100);
|
||||
wfile("audio.volume = 0\naudio.volume_max = 100\naudio.volume_step = 1\n");
|
||||
check("K2 at min: -1 -> 0 (no write)", meow_ui_vol_step(g_path, -1) == 0);
|
||||
check("K2 at min settings 保持 0", vol_in_file() == 0);
|
||||
|
||||
/* ---- K3 保留其它键(不整文件 O_TRUNC)---- */
|
||||
wfile("# meow-settings 1\ndisplay.brightness = 100\naudio.volume = 60\n");
|
||||
meow_ui_vol_step(g_path, +1);
|
||||
check("K3 vol 60 -> 61", vol_in_file() == 61);
|
||||
check("K3 保留其它键 display.brightness=100",
|
||||
meow_ui_vol_read(g_path, "display.brightness", -1) == 100);
|
||||
|
||||
/* ---- K5 媒体键 IPC 兜底:未注册回调 → 走 IPC(无 socket 静默不崩)---- */
|
||||
cb_calls = 0;
|
||||
meow_ui_media_sys_key(165); /* 上一曲:g_cb==NULL 走 IPC 兜底,不崩 */
|
||||
check("K5 未注册回调不崩", cb_calls == 0);
|
||||
|
||||
/* ---- K4 媒体键本地回调:注册后按 cmd 分发 ---- */
|
||||
meow_ui_media_on_playctl(playctl_cb);
|
||||
meow_ui_media_sys_key(165); /* 上一曲 */
|
||||
check("K4 上一曲(165) -> cb(3)", cb_calls == 1 && cb_cmd == 3 && cb_seek == 0);
|
||||
meow_ui_media_sys_key(163); /* 下一曲 */
|
||||
check("K4 下一曲(163) -> cb(4)", cb_calls == 2 && cb_cmd == 4);
|
||||
meow_ui_media_sys_key(19); /* 旧码上一曲 */
|
||||
check("K4 旧码(19) -> cb(3)", cb_calls == 3 && cb_cmd == 3);
|
||||
|
||||
/* 播放/暂停物理键恒发 toggle(6):与最近上报 state 无关(源侧按当前态翻转) */
|
||||
meow_ui_media_meta(1, 0, 0, "t", "a", "al", NULL, NULL); /* state=PLAYING */
|
||||
meow_ui_media_sys_key(164);
|
||||
check("K4 播放中按播放/暂停 -> cb(6) toggle", cb_calls == 4 && cb_cmd == 6);
|
||||
meow_ui_media_meta(2, 0, 0, "t", "a", "al", NULL, NULL); /* state=PAUSED */
|
||||
meow_ui_media_sys_key(164);
|
||||
check("K4 暂停中按播放/暂停 -> cb(6) toggle", cb_calls == 5 && cb_cmd == 6);
|
||||
meow_ui_media_meta(0, 0, 0, "t", "a", "al", NULL, NULL); /* state=STOPPED */
|
||||
meow_ui_media_sys_key(164);
|
||||
check("K4 停止中按播放/暂停 -> cb(6) toggle", cb_calls == 6 && cb_cmd == 6);
|
||||
|
||||
meow_ui_media_stop();
|
||||
|
||||
unlink(g_path);
|
||||
unlink(sock);
|
||||
|
||||
printf("\n%s (%d failure%s)\n", failures ? "RESULT: FAIL" : "RESULT: PASS",
|
||||
failures, failures == 1 ? "" : "s");
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,158 @@
|
||||
#define _GNU_SOURCE
|
||||
|
||||
/* test_ui_audio_host.c — meow_ui_audio SDK 客户端宿主测试。
|
||||
*
|
||||
* 验证:
|
||||
* - meow_audio_open/write/query/pause/resume/close 全链(走 audiod FILE sink)
|
||||
* - on_preempt 回调:第二客户端(raw)抢先 → 本 SDK 客户端收到 PREEMPTED,write 返回负值
|
||||
*
|
||||
* 启动被测试 audiod(宿主二进制,FILE sink)。
|
||||
* 编译:cc -I../meow_launcherd -I. -Os -Wall -Wextra test_ui_audio_host.c meow_ui_audio.c -lpthread -o test_ui_audio_host
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ipc.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#define TSOCK "/tmp/meow_audio_ui.sock"
|
||||
#define TWAV "/tmp/meow_audio_ui.wav"
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void check(int c, const char *n) { if (c) { printf(" PASS %s\n", n); g_pass++; } else { printf(" FAIL %s\n", n); g_fail++; } }
|
||||
|
||||
static int g_preempted = 0;
|
||||
static char g_by[64];
|
||||
static void preempt_cb(const char *by_app, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
g_preempted = 1;
|
||||
snprintf(g_by, sizeof g_by, "%s", by_app ? by_app : "?");
|
||||
}
|
||||
|
||||
/* 小工具:raw 客户端连接 */
|
||||
static int raw_connect(void)
|
||||
{
|
||||
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;
|
||||
snprintf(a.sun_path, sizeof a.sun_path, "%s", TSOCK);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
|
||||
return fd;
|
||||
}
|
||||
static int w_all(int fd, const void *p, size_t n)
|
||||
{
|
||||
const uint8_t *b = p; size_t o = 0;
|
||||
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
|
||||
return 0;
|
||||
}
|
||||
static int r_all(int fd, void *p, size_t n)
|
||||
{
|
||||
uint8_t *b = p; size_t o = 0;
|
||||
while (o < n) { ssize_t r = read(fd, b + o, n - o); if (r <= 0) return -1; o += (size_t)r; }
|
||||
return 0;
|
||||
}
|
||||
static int sf(int fd, uint16_t type, uint32_t req, const void *pl, uint32_t len)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
uint32_t magic = MEOW_IPC_MAGIC; uint16_t ver = MEOW_IPC_VERSION;
|
||||
memcpy(hdr, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &type, 2); memcpy(hdr + 8, &req, 4); memcpy(hdr + 12, &len, 4);
|
||||
if (w_all(fd, hdr, sizeof hdr)) return -1;
|
||||
if (len && pl && w_all(fd, pl, len)) return -1;
|
||||
return 0;
|
||||
}
|
||||
static int rf(int fd, uint16_t *type, uint8_t *rbuf, uint32_t rcap)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
if (r_all(fd, hdr, sizeof hdr)) return -1;
|
||||
uint32_t magic, len; uint16_t ver;
|
||||
memcpy(&magic, hdr, 4); memcpy(&ver, hdr + 4, 2); memcpy(type, hdr + 6, 2); memcpy(&len, hdr + 12, 4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||||
if (len > rcap) len = rcap;
|
||||
if (len && r_all(fd, rbuf, len)) return -1;
|
||||
return (int)len;
|
||||
}
|
||||
static int raw_open(int fd, const char *app, int rate)
|
||||
{
|
||||
m_audio_open_t o; memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "%s", app);
|
||||
o.rate = (uint32_t)rate; o.ch = 2; o.fmt = AUDIO_FMT_S16;
|
||||
if (sf(fd, MSG_AUDIO_OPEN, 9, &o, sizeof o)) return -1;
|
||||
uint8_t rbuf[64]; uint16_t rt;
|
||||
if (rf(fd, &rt, rbuf, sizeof rbuf) < 0 || rt != MSG_AUDIO_OPEN_ACK) return -1;
|
||||
m_audio_open_ack_t a; memcpy(&a, rbuf, sizeof a);
|
||||
return (a.err == MEOW_OK) ? (int)a.stream_id : -1;
|
||||
}
|
||||
|
||||
static int g_audiod_pid = -1;
|
||||
static void stop_audiod(void) { if (g_audiod_pid > 0) { kill(g_audiod_pid, SIGTERM); waitpid(g_audiod_pid, NULL, 0); g_audiod_pid = -1; } }
|
||||
|
||||
int main(void)
|
||||
{
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
setenv("MEOW_AUDIO_SOCK", TSOCK, 1); /* SDK 客户端连接测试 audiod 的 socket */
|
||||
g_audiod_pid = fork();
|
||||
if (g_audiod_pid == 0) {
|
||||
setenv("MEOW_AUDIO_SINK", "file", 1);
|
||||
setenv("MEOW_AUDIO_OUT", TWAV, 1);
|
||||
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
|
||||
setenv("MEOW_AUDIO_FORCE", "1", 1);
|
||||
execl("../meow_audiod/meow_audiod_host", "meow_audiod_host", (char *)0); /* SDK 测试运行于 sdk/ 目录 */
|
||||
_exit(127);
|
||||
}
|
||||
usleep(300000);
|
||||
printf("=== meow_ui_audio SDK host test ===\n");
|
||||
|
||||
/* 注册回调(读线程懒启动) */
|
||||
meow_audio_on_preempt(preempt_cb, NULL);
|
||||
/* 无 audiod 场景:断开后 open 应返回 -ENOENT(先测一次) */
|
||||
int sid = meow_audio_open(44100, 2, AUDIO_FMT_S16);
|
||||
check(sid > 0, "open (audiod present)");
|
||||
|
||||
/* 写正弦(stereo:L/R 同值交织) */
|
||||
static int16_t pcm[4096];
|
||||
for (int i = 0; i < 2048; i++) {
|
||||
int16_t s = (int16_t)(sin(2.0 * M_PI * 440 * i / 44100) * 0.3 * 32767);
|
||||
pcm[i * 2] = s; pcm[i * 2 + 1] = s;
|
||||
}
|
||||
int wr = meow_audio_write(pcm, 2048);
|
||||
check(wr == 2048, "write 2048 frames");
|
||||
|
||||
meow_audio_state st;
|
||||
check(meow_audio_query(&st) == 0 && st.active == 1, "query active");
|
||||
printf(" state: active=%d app=%s rate=%u written=%llu\n", st.active, st.app_id, (unsigned)st.rate, (unsigned long long)st.written_frames);
|
||||
|
||||
check(meow_audio_pause(sid, 1) == 0, "pause");
|
||||
meow_audio_query(&st);
|
||||
check(st.paused == 1, "query paused");
|
||||
check(meow_audio_pause(sid, 0) == 0, "resume");
|
||||
|
||||
/* 第二客户端(raw)抢先 → 本 SDK 收 PREEMPTED,write 返回负值 */
|
||||
int rfd = raw_connect();
|
||||
check(rfd >= 0, "raw connect");
|
||||
int rsid = raw_open(rfd, "raw-app", 96000);
|
||||
check(rsid > 0, "raw open (preempt)");
|
||||
usleep(200000);
|
||||
check(g_preempted == 1, "on_preempt fired");
|
||||
printf(" preempted by %s\n", g_by);
|
||||
check(meow_audio_write(pcm, 2048) < 0, "write after preempt -> <0 (dead)");
|
||||
check(meow_audio_query(&st) == 0, "query still works");
|
||||
|
||||
/* close 旧 sid(已死,应无副作用) */
|
||||
check(meow_audio_close(sid) == 0, "close dead sid");
|
||||
check(meow_audio_pause(sid, 1) < 0 || meow_audio_pause(sid, 1) == -MEOW_ERR_NOENT, "pause dead sid -> error");
|
||||
close(rfd);
|
||||
|
||||
stop_audiod();
|
||||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* test_umbrella.c — 伞头完整性回归:只 #include "meow_ui.h",抽查各模块代表性声明
|
||||
* (类型 / 函数 / 宏)都存在。头拆分后用来证明伞头聚合无遗漏、签名未变。
|
||||
* 用法:gcc -std=c11 -Wall -Wextra -I<sdk> -c test_umbrella.c -o test_umbrella.o
|
||||
* (纯声明级检查,不链接、不调用;产生可执行代码仍 0 warning)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* ---- 类型形状 _Static_assert(编译期)---- */
|
||||
_Static_assert(sizeof(meow_ui_opts) == 2 * sizeof(int), "meow_ui_opts shape");
|
||||
_Static_assert(sizeof(meow_theme) > sizeof(uint32_t), "meow_theme shape");
|
||||
_Static_assert(offsetof(meow_theme, bg) == 0, "meow_theme.bg at 0");
|
||||
_Static_assert(offsetof(meow_ime_ctx, buf) == sizeof(meow_ime_mode), "meow_ime_ctx.buf offset");
|
||||
_Static_assert(offsetof(meow_list_item, title) > 0, "meow_list_item layout");
|
||||
_Static_assert(sizeof(meow_notify) > sizeof(void *), "meow_notify shape");
|
||||
_Static_assert(sizeof(meow_audio_state) > 0, "meow_audio_state shape");
|
||||
_Static_assert(sizeof(meow_widget_opts) > 0, "meow_widget_opts shape");
|
||||
|
||||
/* ---- 枚举/宏值 _Static_assert(编译期;值定格即防漂移)---- */
|
||||
_Static_assert(MEOW_KEY_POWER == 116, "key power");
|
||||
_Static_assert(MEOW_KEY_PREVIOUS == 165, "key previous");
|
||||
_Static_assert(MEOW_SWIPE_DOWN == 3, "swipe down");
|
||||
_Static_assert(MEOW_IME_PINYIN == 2, "ime pinyin");
|
||||
_Static_assert(MEOW_THEME_DARK == 1, "theme dark");
|
||||
_Static_assert(MEOW_STYLE_MIUIX == 1, "style miuix");
|
||||
_Static_assert(MEOW_Z_TOP == 1000, "z top");
|
||||
_Static_assert(MEOW_TOPBAR_BACK == (-2), "topbar back");
|
||||
_Static_assert(MEOW_SBAR_HOTZONE_FS == 4, "sbar hotzone fs");
|
||||
_Static_assert(MEOW_WS_PRESSED == (1u << 3), "ws pressed");
|
||||
_Static_assert(MEOW_BTN_ELEVATED == 4, "btn elevated");
|
||||
_Static_assert(MEOW_LAYOUT_BOX == 2, "layout box");
|
||||
_Static_assert(MEOW_LIST_SEL_MULTI == 2, "list sel multi");
|
||||
_Static_assert(MEOW_NOTIFY_CAT_MEDIA == 5, "notify cat media");
|
||||
_Static_assert(MEOW_NOTIFY_URG_CRITICAL == 3, "notify urg critical");
|
||||
_Static_assert(MEOW_NOTIFY_TIMEOUT_ONGOING == (-1), "notify ongoing");
|
||||
_Static_assert(MEOW_LOG_ERROR == 3, "log error");
|
||||
_Static_assert(MEOW_DLG_INPUT == 2, "dlg input");
|
||||
_Static_assert(MEOW_FONT_TIER_FULL == 2, "font tier full");
|
||||
_Static_assert(MEOW_CFG_ERR_NOENT == (-2), "cfg noent");
|
||||
|
||||
/* ---- 函数存在性检查(取址即要求已声明;未声明 → 编译错误)---- */
|
||||
static void umbrella_smoke(void)
|
||||
{
|
||||
/* 绘制层(§3)+ 时间/刷新率(§7/§7.1) */
|
||||
(void)&meow_ui_init; (void)&meow_ui_flush; (void)&meow_ui_fill_rect_alpha;
|
||||
(void)&meow_ui_text_center; (void)&meow_ui_time_ms; (void)&meow_ui_effective_rate;
|
||||
/* 图片层(§4) */
|
||||
(void)&meow_img_load_file; (void)&meow_ui_img_cover; (void)&meow_img_w;
|
||||
/* 主题(§8) */
|
||||
(void)&meow_theme_get; (void)&meow_theme_defaults; (void)&meow_ui_set_style;
|
||||
/* 输入法(§5.1) */
|
||||
(void)&meow_ui_text_input; (void)&meow_ime_text; (void)&meow_ime_composition;
|
||||
/* 控件层(§5/§5.8/§5.8.6/§5.5) */
|
||||
(void)&meow_btn_create; (void)&meow_widget_custom_create; (void)&meow_row_create;
|
||||
(void)&meow_topappbar_hit; (void)&meow_listview_create; (void)&meow_list_tap_at;
|
||||
(void)&meow_widget_set_opts; (void)&meow_widget_state; (void)&meow_progress_set;
|
||||
/* 输入层/生命周期(§6)+ 长按(§6.3) */
|
||||
(void)&meow_ui_run; (void)&meow_ui_swipe_hint; (void)&meow_ui_set_long_press_cb;
|
||||
/* Activity 栈(§6.1) */
|
||||
(void)&meow_ui_activity_push; (void)&meow_ui_global_add; (void)&meow_ui_activity_depth;
|
||||
/* 绘制物注册表 / z 序(§6.2) */
|
||||
(void)&meow_widget_set_z; (void)&meow_draw_custom_register; (void)&meow_ui_invalidate;
|
||||
(void)&meow_ui_paint_full; (void)&meow_ui_set_page_bg;
|
||||
/* 状态栏 / 全屏(§3.1) */
|
||||
(void)&meow_ui_set_statusbar; (void)&meow_ui_statusbar_height; (void)&meow_ui_statusbar_hotzone;
|
||||
(void)&meow_ui_statusbar_set_collapsed;
|
||||
/* 模态(§5.8.6 弹窗/面板) */
|
||||
(void)&meow_dialog_create; (void)&meow_dialog_show; (void)&meow_sheet_open;
|
||||
/* 日志(§9) */
|
||||
(void)&meow_log; (void)&meow_log_get_level;
|
||||
/* 通知(§9.1) */
|
||||
(void)&meow_notify_send; (void)&meow_notify_category_str; (void)&meow_notify_update_progress;
|
||||
/* 配置(§9.2) */
|
||||
(void)&meow_config_dir; (void)&meow_config_int; (void)&meow_config_remove;
|
||||
/* 字体(§5.7/§10) */
|
||||
(void)&meow_font_enumerate; (void)&meow_font_tier; (void)&meow_font_select_by_id;
|
||||
/* 音频(§11) */
|
||||
(void)&meow_audio_open; (void)&meow_audio_query; (void)&meow_audio_on_preempt;
|
||||
}
|
||||
|
||||
/* 供宿主回归驱动直接调用;不链接本库也没关系(纯地址丢弃)。 */
|
||||
void test_umbrella_run(void) { umbrella_smoke(); }
|
||||
Reference in new issue
Block a user