diff --git a/sdk/build_sdk.sh b/sdk/build_sdk.sh new file mode 100755 index 0000000..8c67ac6 --- /dev/null +++ b/sdk/build_sdk.sh @@ -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 diff --git a/sdk/gen_pinyin_lex.py b/sdk/gen_pinyin_lex.py new file mode 100644 index 0000000..6cd2840 --- /dev/null +++ b/sdk/gen_pinyin_lex.py @@ -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 ') + 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() diff --git a/sdk/include/meow_ui_activity.h b/sdk/include/meow_ui_activity.h new file mode 100644 index 0000000..7885e5d --- /dev/null +++ b/sdk/include/meow_ui_activity.h @@ -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 */ diff --git a/sdk/include/meow_ui_audio.h b/sdk/include/meow_ui_audio.h new file mode 100644 index 0000000..15b7a07 --- /dev/null +++ b/sdk/include/meow_ui_audio.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 */ diff --git a/sdk/include/meow_ui_config.h b/sdk/include/meow_ui_config.h new file mode 100644 index 0000000..9e957a7 --- /dev/null +++ b/sdk/include/meow_ui_config.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//data, + * 见 app_launcher.c launch_app);无 env(手动跑/宿主测/无卡)回退 /tmp/meowcfg_ + * (非持久但仍可写)。meow_config_dir() 返回最终目录;空/不可建返回 NULL。 + * - 文件布局:每键一小文件 /.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);/* 写 /.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 */ diff --git a/sdk/include/meow_ui_dialog.h b/sdk/include/meow_ui_dialog.h new file mode 100644 index 0000000..ae49a38 --- /dev/null +++ b/sdk/include/meow_ui_dialog.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 */ diff --git a/sdk/include/meow_ui_draw.h b/sdk/include/meow_ui_draw.h new file mode 100644 index 0000000..e6ff303 --- /dev/null +++ b/sdk/include/meow_ui_draw.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 */ diff --git a/sdk/include/meow_ui_font.h b/sdk/include/meow_ui_font.h new file mode 100644 index 0000000..87f29fb --- /dev/null +++ b/sdk/include/meow_ui_font.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/// 后落盘的持久文件 + * 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// 一级目录名 */ + 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///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 */ diff --git a/sdk/include/meow_ui_image.h b/sdk/include/meow_ui_image.h new file mode 100644 index 0000000..88c15e0 --- /dev/null +++ b/sdk/include/meow_ui_image.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 */ diff --git a/sdk/include/meow_ui_ime.h b/sdk/include/meow_ui_ime.h new file mode 100644 index 0000000..222dc1e --- /dev/null +++ b/sdk/include/meow_ui_ime.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 */ diff --git a/sdk/include/meow_ui_input.h b/sdk/include/meow_ui_input.h new file mode 100644 index 0000000..956dbe1 --- /dev/null +++ b/sdk/include/meow_ui_input.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 */ diff --git a/sdk/include/meow_ui_log.h b/sdk/include/meow_ui_log.h new file mode 100644 index 0000000..5a18887 --- /dev/null +++ b/sdk/include/meow_ui_log.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 管「写哪/怎么写/过滤/降级/上限」: + * - 路径:/<启动时间>_.log(MEOW_LOG_DIR 已含包名分层 logs/<包名>) + * (目录不存在 mkdir -p,按 app 分目录),每次启动=新文件,同一次启动内 O_APPEND 追加; + * - 级别过滤:lvl < meow_log_get_level() 丢弃;默认 MEOW_LOG_INFO; + * - 格式: [] message\n; + * - 上限:单文件 ≥2MB 自动开新文件追加 _;每 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 */ diff --git a/sdk/include/meow_ui_notify.h b/sdk/include/meow_ui_notify.h new file mode 100644 index 0000000..9795363 --- /dev/null +++ b/sdk/include/meow_ui_notify.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 */ diff --git a/sdk/include/meow_ui_statusbar.h b/sdk/include/meow_ui_statusbar.h new file mode 100644 index 0000000..3f5c74f --- /dev/null +++ b/sdk/include/meow_ui_statusbar.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 */ diff --git a/sdk/include/meow_ui_theme.h b/sdk/include/meow_ui_theme.h new file mode 100644 index 0000000..ebc80b0 --- /dev/null +++ b/sdk/include/meow_ui_theme.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 */ diff --git a/sdk/include/meow_ui_types.h b/sdk/include/meow_ui_types.h new file mode 100644 index 0000000..61ab1f4 --- /dev/null +++ b/sdk/include/meow_ui_types.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 +#include + +#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 */ diff --git a/sdk/include/meow_ui_widgets.h b/sdk/include/meow_ui_widgets.h new file mode 100644 index 0000000..ee9f476 --- /dev/null +++ b/sdk/include/meow_ui_widgets.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 */ diff --git a/sdk/include/meow_ui_zorder.h b/sdk/include/meow_ui_zorder.h new file mode 100644 index 0000000..86711e1 --- /dev/null +++ b/sdk/include/meow_ui_zorder.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 */ diff --git a/sdk/meow_notify_ipc.c b/sdk/meow_notify_ipc.c new file mode 100644 index 0000000..5445b6a --- /dev/null +++ b/sdk/meow_notify_ipc.c @@ -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 /* snprintf */ +#include /* getenv */ +#include +#include /* EINTR */ +#include +#include +#include +#include +#include + +/* 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); +} diff --git a/sdk/meow_notify_ipc.h b/sdk/meow_notify_ipc.h new file mode 100644 index 0000000..14df264 --- /dev/null +++ b/sdk/meow_notify_ipc.h @@ -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 +#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 */ diff --git a/sdk/meow_pinyin.c b/sdk/meow_pinyin.c new file mode 100644 index 0000000..343c6a8 --- /dev/null +++ b/sdk/meow_pinyin.c @@ -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 +#include + +#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; +} diff --git a/sdk/meow_pinyin.h b/sdk/meow_pinyin.h new file mode 100644 index 0000000..5a1fa33 --- /dev/null +++ b/sdk/meow_pinyin.h @@ -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 */ diff --git a/sdk/meow_pinyin_lex.h b/sdk/meow_pinyin_lex.h new file mode 100644 index 0000000..c00d2ed --- /dev/null +++ b/sdk/meow_pinyin_lex.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 + +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 */ diff --git a/sdk/meow_ui.h b/sdk/meow_ui.h new file mode 100644 index 0000000..cfbec99 --- /dev/null +++ b/sdk/meow_ui.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 +#include + +#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 */ diff --git a/sdk/meow_ui_activity.c b/sdk/meow_ui_activity.c new file mode 100644 index 0000000..84f29ef --- /dev/null +++ b/sdk/meow_ui_activity.c @@ -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; +} diff --git a/sdk/meow_ui_audio.c b/sdk/meow_ui_audio.c new file mode 100644 index 0000000..eb7a008 --- /dev/null +++ b/sdk/meow_ui_audio.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#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; +} diff --git a/sdk/meow_ui_config.c b/sdk/meow_ui_config.c new file mode 100644 index 0000000..ce99fa7 --- /dev/null +++ b/sdk/meow_ui_config.c @@ -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//data, + * 见 app_launcher.c launch_app,exFAT 无 symlink → env 传址);无 env(手动跑/宿主测/无卡) + * 回退 /tmp/meowcfg_(非持久但仍可写); + * - 文件布局:每键一小文件 /.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 +#include +#include +#include +#include +#include +#include +#include +#include + +/* 内部常量(轻量键值小文件,见头注释) */ +#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) /* /.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_。 */ + 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; + } + + /* 去尾斜杠(统一 /.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 指向 /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; +} + +/* 拼 /.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); +} diff --git a/sdk/meow_ui_dialog.c b/sdk/meow_ui_dialog.c new file mode 100644 index 0000000..5d06425 --- /dev/null +++ b/sdk/meow_ui_dialog.c @@ -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 +#include +#include +#include + +#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); } diff --git a/sdk/meow_ui_draw.c b/sdk/meow_ui_draw.c new file mode 100644 index 0000000..4d4f05d --- /dev/null +++ b/sdk/meow_ui_draw.c @@ -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 +#include + +#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); +} diff --git a/sdk/meow_ui_gfx.c b/sdk/meow_ui_gfx.c new file mode 100644 index 0000000..f459632 --- /dev/null +++ b/sdk/meow_ui_gfx.c @@ -0,0 +1,1252 @@ +/* + * meow_ui_gfx.c — meow_ui 渲染后端(B1.0+B1.1)。 + * + * 独立实现:自建 fb 上下文(自 mmap /dev/fb0、自建 back_buf、自实现脏区+present), + * 不复用 launcher 的全局 g_dirty/fb(launcher 与 app 是不同进程)。机制参考 + * MEOW_APP_SDK.md §1/§2 与 launcher 的 M2.16 无闪烁地基,代码全部独立书写。 + * + * 抽象:所有绘制目标 = 离屏 back_buf(g_fb);meow_ui_flush() 时单次原子 + * vsync+blit 到 fb0(真实设备),或写出 PPM 截图(宿主/qemu 离屏,MEOW_UI_DUMP)。 + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" +#include "meow_ui_sys.h" +#include "sb_icons.h" +#include "../meow_launcherd/meow_font.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 本设备实测 ioctl 号(勿与通用头交换) */ +#define FBIOGET_VSCREENINFO 0x4600 +#define FBIOGET_FSCREENINFO 0x4602 +#define FBIO_WAITFORVSYNC 0x4620 + +#define FB_DEV "/dev/fb0" +#define MEOW_FONT_PNG_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.png" +#define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowOS/fonts/default/regular/font.idx" +#define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png" +#define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx" +/* 默认字库(卡 MeowOS/fonts/default/regular,M2.6 字重结构)与固件内置之外,launcher 会把 + * 「当前生效字体」经环境变量下发给 app(见 meow_launcherd/app_launcher.c `launch_app` / font_theme.c `meow_font_effective_paths`): + * MEOW_FONT_PNG / MEOW_FONT_IDX = 绝对路径;MEOW_FONT_APP = 或 "default"。 + * SDK load_font 优先读 env(匹配系统 font.file),无则回退卡默认 → 固件。 */ + +#define MUI_DEFAULT_W 480 +#define MUI_DEFAULT_H 720 + +/* §10.5 叠加让位标记:与 launcher meow_launcher_internal.h NOTIF_ACTIVE 同路径。 + * 通知中心叠加会话期存在;宿主/单测可经 MEOW_NOTIF_ACTIVE_FILE 覆写路径(进程内只解析一次)。 */ +#define MEOW_NOTIF_ACTIVE_DEFAULT "/tmp/meow_notif_active" +#define MEOW_OVERLAY_CHK_MS 200 /* 存在性检查节流:别每帧 stat */ + +/* 脏矩形上限(超出合并为全屏) */ +#define MUI_DIRTY_MAX 12 + +struct mu_rect { int x, y, w, h; }; + +/* ---- fb 上下文 ---- */ +static unsigned char *g_back_buf = NULL; /* 离屏(所有绘制目标) */ +static unsigned char *g_fb_mem = NULL; /* 真实 mmap 帧缓冲(present 落点;NULL=离屏) */ +static unsigned char *g_fb = NULL; /* 绘制指针 = g_back_buf */ +static int g_fb_fd = -1; +static int g_xres = MUI_DEFAULT_W, g_yres = MUI_DEFAULT_H; +static int g_bpp = 16; +static int g_line = 0; /* 每行字节数 */ +static int g_init_ok = 0; +static int g_mfont_ready = 0; + +static int g_dirty[/*MUI_DIRTY_MAX*/12][4]; /* [x,y,w,h] */ +static int g_ndirty = 0; +static int g_dirty_full = 1; + +/* ---- 绘制期裁剪(clip rect,§3;默认全屏)---- + * 栈式嵌套:push 与当前 clip 求交后压栈,pop 恢复。写像素原语(put_px / put_px_blend / + * fill_rect / fill_rect_alpha / round / circle / ring / blit_icon / blit_rgba / text) + * 统一过 g_clip;顶层(g_clip_n==0)= 全屏(g_xres×g_yres),故 IME/模态的全屏 scrim + * 直绘不受裁剪。 */ +#define MUI_CLIP_MAX 16 +static int g_clip_stack[MUI_CLIP_MAX][4]; /* [x0,y0,x1,y1](闭开区间) */ +static int g_clip_n = 0; + +static void clip_cur(int out[4]) { /* 当前 clip 边界(无 clip = 全屏) */ + if (g_clip_n > 0) { + out[0] = g_clip_stack[g_clip_n - 1][0]; out[1] = g_clip_stack[g_clip_n - 1][1]; + out[2] = g_clip_stack[g_clip_n - 1][2]; out[3] = g_clip_stack[g_clip_n - 1][3]; + } else { out[0] = 0; out[1] = 0; out[2] = g_xres; out[3] = g_yres; } +} +/* 逐像素 clip 判定:须 inline —— MIPS -Os 若抽成函数调用,每一像素(ring/circle/圆角/ + * alpha/blit/5x7)都付一次 call+prologue,软渲上被显著放大(宿主 x86 -Os 会内联故测不出)。 + * 强制内联后为 4 次整数比较,与旧「越界检查」同价;顶层(g_clip_n==0)= 全屏零开销直通。 */ +static inline int clip_contains(int x, int y) +{ + if (g_clip_n == 0) return x >= 0 && x < g_xres && y >= 0 && y < g_yres; + return x >= g_clip_stack[g_clip_n - 1][0] && x < g_clip_stack[g_clip_n - 1][2] && + y >= g_clip_stack[g_clip_n - 1][1] && y < g_clip_stack[g_clip_n - 1][3]; +} + +void meow_gfx_clip_push(int x, int y, int w, int h) +{ + if (!g_init_ok || w <= 0 || h <= 0) return; + int c[4]; clip_cur(c); + int ix0 = x > c[0] ? x : c[0]; + int iy0 = y > c[1] ? y : c[1]; + int ix1 = (x + w) < c[2] ? (x + w) : c[2]; + int iy1 = (y + h) < c[3] ? (y + h) : c[3]; + if (ix1 < ix0) ix1 = ix0; + if (iy1 < iy0) iy1 = iy0; + if (g_clip_n >= MUI_CLIP_MAX) return; /* 栈满:忽略本次 push(保持当前 clip) */ + g_clip_stack[g_clip_n][0] = ix0; g_clip_stack[g_clip_n][1] = iy0; + g_clip_stack[g_clip_n][2] = ix1; g_clip_stack[g_clip_n][3] = iy1; + g_clip_n++; +} +void meow_gfx_clip_pop(void) +{ + if (g_clip_n > 0) g_clip_n--; +} + +static char *g_dump_path = NULL; + +/* ---- 状态栏「上收出屏只留提示线」(collapsed,基座)---- + * g_sbar_collapsed = 目标态(1=上收);g_sbar_collapse_h = 当前实际上收偏移 + * (0..MUI_SBAR_H:0=完全展开,MUI_SBAR_H=内容全部上移出屏、仅留提示线)。 + * 基座让 meow_ui_statusbar_set_collapsed(on) 立即生效(true→只画提示线/内容上移); + * 过渡(每帧插值)留待后续代理或经 meow_ui_statusbar_animate(ms) 启用的简易线性逼近。 + * 「收合=只留提示线」占高 = MUI_SBAR_COLLAPSE_H(=MEOW_SBAR_HOTZONE_FS=4,meow_ui_sys.h), + * 与 inset(meow_ui_statusbar_height())/顶部下滑热区/收合渲染同口径——replaced MUI_SBAR_HINT_H。 */ +static int g_sbar_collapsed = 0; /* 目标:1=上收 */ +static int g_sbar_collapse_h = 0; /* 当前上收偏移(0..MUI_SBAR_H) */ +static int g_sbar_collapse_anim_ms = 0; /* 过渡时长(0=瞬切);meow_ui_statusbar_animate 设置 */ +static int g_sbar_collapse_from_h = 0; /* 本次过渡起点偏移 */ +static unsigned long long g_sbar_collapse_start_ms = 0; /* 本次过渡起始(monotonic ms) */ + +/* ---- 字体档位(MEOW_FONT_TIER,基座存档 + 公共 get/set)---- + * launcher 按 app.json "fonts":<档> 注入 MEOW_FONT_TIER=<0|1|2>;SDK 读入并以 + * meow_font_set_tier/tier 暴露。按档装载大字库(GBK/Full)由 meow_font_reload_tier() + * 在 meow_font.c 实现;meow_font_set_tier() 在档位变化时经 env/settings 路由到该钩子真重载。 */ +static int g_font_tier = MEOW_FONT_TIER_COMMON; + +/* ---- 帧率调度(A-1/A-2:从 launcher M2.16 迁入,刷新率调度归口 SDK)---- + * meow_ui_flush()/present() 的 fps 档位与自适应降档 governor。launcher 不再自带 + * 一套帧率门控;launcher 与 app 统一走这里 → 全局观感/刷新率一致。 + * 设置接口:meow_ui_set_refresh_rate()/meow_ui_set_refresh_always_max()。 */ +static int g_refresh_rate_cfg = 60; /* 配置目标档 60/30/15(上限) */ +static int g_eff_rate = 60; /* 当前有效帧率(governor 可降档) */ +static int g_refresh_always_max = 0; /* 1=恒 max 不降档 */ +static unsigned long long g_last_present_ms = 0; +static unsigned long long g_frames_presented = 0; +static unsigned long long g_present_cost_sum = 0; +static int g_present_cost_n = 0; +static int g_gov_low = 0; + +/* 每档上限(refresh_always_max=1 时恒 max;0 时 governor 可在 [15,30,60] 内降/升档)。 */ +static int rate_bump(int r) { return (r == 15) ? 30 : (r == 30) ? 60 : 60; } +static int rate_drop(int r) { return (r == 60) ? 30 : (r == 30) ? 15 : 15; } + +/* 自适应降档 governor:统计每帧 present 耗时;连续 12 帧平均超预算 → 降一档; + * 连续 4 批(≈48 帧)平均远低于预算 → 升一档。refresh_always_max=1 时恒 max 不降。 */ +static void refresh_governor(unsigned long long cost_ms, int period) +{ + if (g_refresh_always_max || g_eff_rate <= 0) return; + g_present_cost_sum += cost_ms; g_present_cost_n++; + if (g_present_cost_n < 12) return; + unsigned long long avg = g_present_cost_sum / (unsigned int)g_present_cost_n; + if (avg > (unsigned long long)period) { + if (g_eff_rate > 15) g_eff_rate = rate_drop(g_eff_rate); + g_gov_low = 0; + } else if (avg * 2 < (unsigned long long)period) { + if (++g_gov_low >= 4) { + int nr = rate_bump(g_eff_rate); + if (nr <= g_refresh_rate_cfg) g_eff_rate = nr; + g_gov_low = 0; + } + } else g_gov_low = 0; + g_present_cost_sum = 0; g_present_cost_n = 0; +} + +/* ---- 工具 ---- */ +static uint64_t now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; +} + +uint64_t meow_ui_time_ms(void) { return now_ms(); } + +static uint16_t rgb565(uint32_t c) +{ + return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)); +} + +static uint8_t ex5(uint16_t v) { return (uint8_t)(((v) << 3) | ((v) >> 2)); } /* 5→8 */ +static uint8_t ex6(uint16_t v) { return (uint8_t)(((v) << 2) | ((v) >> 4)); } /* 6→8 */ + +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 void dirty_full(void) { g_dirty_full = 1; g_ndirty = 0; } +static void dirty_reset(void) { g_ndirty = 0; g_dirty_full = 0; } + +static void dirty_add(int x, int y, int w, int h) +{ + if (g_dirty_full) return; + if (w <= 0 || h <= 0) return; + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x >= g_xres || y >= g_yres) return; + if (x + w > g_xres) w = g_xres - x; + if (y + h > g_yres) h = g_yres - y; + if (w <= 0 || h <= 0) return; + /* 绘制期裁剪:脏区也不越 clip(顶层 clip=全屏 → 无操作)。 */ + if (g_clip_n > 0) { + int *c = g_clip_stack[g_clip_n - 1]; + int x1 = x + w, y1 = y + h; + if (x < c[0]) x = c[0]; + if (y < c[1]) y = c[1]; + if (x1 > c[2]) x1 = c[2]; + if (y1 > c[3]) y1 = c[3]; + if (x1 <= x || y1 <= y) return; + w = x1 - x; h = y1 - y; + } + for (int i = 0; i < g_ndirty; i++) { + if (rect_hit(g_dirty[i][0], g_dirty[i][1], g_dirty[i][2], g_dirty[i][3], x, y, w, h)) { + int nx1 = g_dirty[i][0] < x ? g_dirty[i][0] : x; + int ny1 = g_dirty[i][1] < y ? g_dirty[i][1] : y; + int nx2 = (g_dirty[i][0] + g_dirty[i][2]) > (x + w) ? (g_dirty[i][0] + g_dirty[i][2]) : (x + w); + int ny2 = (g_dirty[i][1] + g_dirty[i][3]) > (y + h) ? (g_dirty[i][1] + g_dirty[i][3]) : (y + h); + g_dirty[i][0] = nx1; g_dirty[i][1] = ny1; + g_dirty[i][2] = nx2 - nx1; g_dirty[i][3] = ny2 - ny1; + return; + } + } + if (g_ndirty >= MUI_DIRTY_MAX) { dirty_full(); return; } + g_dirty[g_ndirty][0] = x; g_dirty[g_ndirty][1] = y; + g_dirty[g_ndirty][2] = w; g_dirty[g_ndirty][3] = h; + g_ndirty++; +} + +void meow_gfx_mark_dirty(int x, int y, int w, int h) { dirty_add(x, y, w, h); } +void meow_gfx_mark_full(void) { dirty_full(); } +void meow_gfx_dirty_reset(void) { dirty_reset(); } + +/* flush 挂起(IME/模态阻塞循环用):挂起期间 flush/flush_region 只记脏不上屏, + * 解除后由调用方一次 flush 原子上屏——避免「App on_tick 重绘先上屏、覆盖层随后再上屏」 + * 同一区域一帧两刷的频闪。 */ +static int g_flush_hold = 0; +void meow_gfx_flush_hold(int on) { g_flush_hold = on ? 1 : 0; } + +/* back_buf 每行字节数 / 位深(M2.71-bug3:模态/IME 整帧快照恢复用)。 */ +int meow_ui_back_buf_pitch(void) { return g_line; } +int meow_ui_back_buf_bpp(void) { return g_bpp; } + +/* launcher(系统组件)与 SDK 共用同一 back_buf:launcher 的 fb 绘制指针指向它, + * 从而 launcher 与 app 统一渲染栈(同一离屏 + 同一 present)。 */ +unsigned char *meow_ui_back_buf(void) { return g_back_buf; } + +/* ================= 状态栏 MD3 图标 blit(写进本 SDK back_buf) ================= + * 复用 launcher sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)作为状态栏 + * 右侧图标簇(「同一个状态栏」图标式,见 meow_ui_sys_statusbar_render)。 + * idx = sb_icons[] 索引(SB_I_*,见 meow_ui_internal.h) + * x = 图标槽左缘(槽宽 SB_ICON_SIZE=32) + * y = 图标槽顶(仅作参考;实际按 ink 垂直中心对齐 cy) + * ink_h = 目标 ink 高(>0 等比缩放;0 = 保持位图自然 ink 尺寸——WiFi 强弱档用) + * fg = 24-bit RGB 前景(主题 on_surface / on_surface_variant,日/夜由主题表提供) + * cy = ink 垂直中心线(= 状态栏文字中心,28px 栏约 14) + * 先求位图 ink 边界(阈值 32,忽略近透明抗锯齿),按 ink 在槽内水平居中、垂直中心对齐 cy; + * 像素按 alpha 软边混合进 back_buf(自 g_fb/g_line/g_xres/g_yres,与 launcher sb_blit_icon 同源算法)。 */ +void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy) +{ + (void)y; + if (!g_init_ok || !g_fb) return; + if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return; + const unsigned char *src = sb_icons[idx].data; + uint16_t col = rgb565(fg); + /* 1) ink 边界(含空图兜底) */ + int minx = SB_ICON_SIZE, miny = SB_ICON_SIZE, maxx = -1, maxy = -1; + for (int i = 0; i < SB_ICON_SIZE * SB_ICON_SIZE; i++) { + if (src[i] >= 32) { + int xx = i % SB_ICON_SIZE, yy = i / SB_ICON_SIZE; + if (xx < minx) minx = xx; if (xx > maxx) maxx = xx; + if (yy < miny) miny = yy; if (yy > maxy) maxy = yy; + } + } + if (maxx < minx || maxy < miny) { minx = miny = 0; maxx = maxy = SB_ICON_SIZE - 1; } + int ink_w = maxx - minx + 1, ink_h_n = maxy - miny + 1; + int t_h = (ink_h > 0) ? ink_h : ink_h_n; + int t_w = ink_w * t_h / ink_h_n; if (t_w < 1) t_w = 1; + if (t_w > SB_ICON_SIZE * 2) t_w = SB_ICON_SIZE * 2; /* 防御:等比极端上限 */ + /* 2) 目标框左上:水平居中于 32px 槽;垂直中心 = cy */ + int x0 = x + (SB_ICON_SIZE - t_w) / 2; + int y0 = cy - t_h / 2; + /* 3) 最近邻采样映射:目标 (dx,dy) ← 源 ink */ + for (int dy = 0; dy < t_h; dy++) { + int ty = y0 + dy; + if (ty < 0 || ty >= g_yres) continue; + int sy = miny + (dy * ink_h_n) / t_h; + uint16_t *row = (uint16_t *)(g_fb + (size_t)ty * g_line); + for (int dx = 0; dx < t_w; dx++) { + int tx = x0 + dx; + if (tx < 0 || tx >= g_xres) continue; + if (!clip_contains(tx, ty)) continue; + int sx = minx + (dx * ink_w) / t_w; + int a = src[sy * SB_ICON_SIZE + sx]; + if (a < 8) continue; /* 透明像素跳过 */ + uint16_t bg = row[tx]; + if (a >= 240) { row[tx] = col; continue; } + int a4 = a >> 4; /* 0..15 */ + int r = ((col >> 11) * a4 + ((bg >> 11) & 0x1F) * (16 - a4)) >> 4; + int g = (((col >> 5) & 0x3F) * a4 + ((bg >> 5) & 0x3F) * (16 - a4)) >> 4; + int b = ((col & 0x1F) * a4 + (bg & 0x1F) * (16 - a4)) >> 4; + row[tx] = (uint16_t)((r << 11) | (g << 5) | b); + } + } + /* 整条顶栏作为脏区标出(图标在顶条内,present 只重画顶条即可) */ + dirty_add(x, 0, SB_ICON_SIZE, MUI_SBAR_H); +} + +/* ================= 像素写入 ================= */ +static void px_write_rgb565(int x, int y, uint16_t c16) +{ + *(uint16_t *)(g_fb + (size_t)y * g_line + (size_t)x * 2) = c16; +} +static uint16_t px_read_rgb565(int x, int y) +{ + return *(uint16_t *)(g_fb + (size_t)y * g_line + (size_t)x * 2); +} + +static void put_px(int x, int y, uint32_t color) +{ + if (!clip_contains(x, y)) return; + if (g_bpp == 16) { + px_write_rgb565(x, y, rgb565(color)); + } else { + unsigned char *p = g_fb + (size_t)y * g_line + (size_t)x * 4; + *(uint32_t *)p = (color << 8) | 0xFF; + } +} + +static void put_px_blend(int x, int y, uint32_t color, int alpha) +{ + if (!clip_contains(x, y)) return; + uint16_t fg = rgb565(color); + if (g_bpp == 16) { + uint16_t bg = px_read_rgb565(x, y); + int ia = 255 - alpha; + unsigned r = ((((fg >> 11) & 0x1F) * alpha + ((bg >> 11) & 0x1F) * ia) / 255); + unsigned gg = ((((fg >> 5) & 0x3F) * alpha + ((bg >> 5) & 0x3F) * ia) / 255); + unsigned b = (((fg & 0x1F) * alpha + (bg & 0x1F) * ia) / 255); + px_write_rgb565(x, y, (uint16_t)((r << 11) | (gg << 5) | b)); + } else { + unsigned char *p = g_fb + (size_t)y * g_line + (size_t)x * 4; + uint32_t bg = (*(uint32_t *)p) >> 8; + int ia = 255 - alpha; + unsigned r = ((((color >> 16) & 0xFF) * alpha + ((bg >> 16) & 0xFF) * ia) / 255); + unsigned gg = ((((color >> 8) & 0xFF) * alpha + ((bg >> 8) & 0xFF) * ia) / 255); + unsigned b = (((color & 0xFF) * alpha + (bg & 0xFF) * ia) / 255); + *(uint32_t *)p = (r << 24) | (gg << 16) | (b << 8) | 0xFF; + } +} + +/* ================= 基本绘制 ================= */ +void meow_ui_clear(uint32_t color) +{ + if (!g_init_ok) return; + if (g_clip_n > 0) { /* 绘制期裁剪:清当前 clip 区(顶层 clip=全屏 → 走快速整刷) */ + meow_ui_fill_rect(0, 0, g_xres, g_yres, color); + return; + } + /* 快速整刷(16bit 用半字填充,可 memset 当色值由 32bit 拼接掩码) */ + if (g_bpp == 16) { + uint16_t c16 = rgb565(color); + uint32_t c32 = (uint32_t)c16 | ((uint32_t)c16 << 16); + for (int y = 0; y < g_yres; y++) { + uint32_t *row = (uint32_t *)(g_fb + (size_t)y * g_line); + int n = g_xres / 2; + for (int i = 0; i < n; i++) row[i] = c32; + if (g_xres & 1) px_write_rgb565(g_xres - 1, y, c16); + } + } else { + for (int y = 0; y < g_yres; y++) + for (int x = 0; x < g_xres; x++) put_px(x, y, color); + } + dirty_full(); +} + +void meow_ui_fill_rect(int x, int y, int w, int h, uint32_t color) +{ + if (!g_init_ok) return; + if (w <= 0 || h <= 0) return; + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x >= g_xres || y >= g_yres) return; + if (x + w > g_xres) w = g_xres - x; + if (y + h > g_yres) h = g_yres - y; + if (w <= 0 || h <= 0) return; + if (g_clip_n > 0) { /* 绘制期裁剪:与当前 clip 求交(顶层 clip=全屏 → 无操作) */ + int *c = g_clip_stack[g_clip_n - 1]; + int x1 = x + w, y1 = y + h; + if (x < c[0]) x = c[0]; + if (y < c[1]) y = c[1]; + if (x1 > c[2]) x1 = c[2]; + if (y1 > c[3]) y1 = c[3]; + if (x1 <= x || y1 <= y) return; + w = x1 - x; h = y1 - y; + } + if (g_bpp == 16) { + uint16_t c16 = rgb565(color); + for (int ry = y; ry < y + h; ry++) { + uint16_t *rp = (uint16_t *)(g_fb + (size_t)ry * g_line + (size_t)x * 2); + for (int rx = 0; rx < w; rx++) rp[rx] = c16; + } + } else { + for (int ry = y; ry < y + h; ry++) + for (int rx = x; rx < x + w; rx++) put_px(rx, ry, color); + } + dirty_add(x, y, w, h); +} + +void meow_ui_put_px(int x, int y, uint32_t color) +{ + if (!g_init_ok) return; + put_px(x, y, color); + dirty_add(x, y, 1, 1); +} + +void meow_ui_draw_rect(int x, int y, int w, int h, uint32_t color) +{ + if (w <= 0 || h <= 0) return; + meow_ui_fill_rect(x, y, w, 1, color); + meow_ui_fill_rect(x, y + h - 1, w, 1, color); + meow_ui_fill_rect(x, y, 1, h, color); + meow_ui_fill_rect(x + w - 1, y, 1, h, color); +} + +/* 圆角矩形区域判定(含内缩) */ +static int rr_inside(int px, int py, int x, int y, int w, int h, int r) +{ + if (px < x || px >= x + w || py < y || py >= y + h) return 0; + if (r <= 0) return 1; + if (w < 2 * r || h < 2 * r) return 1; /* 半径过大退化为矩形 */ + int cx1 = x + r, cy1 = y + r; + int cx2 = x + w - 1 - r, cy2 = y + h - 1 - r; + if (px < cx1 && py < cy1) return ((px - cx1) * (px - cx1) + (py - cy1) * (py - cy1)) <= r * r; + if (px > cx2 && py < cy1) return ((px - cx2) * (px - cx2) + (py - cy1) * (py - cy1)) <= r * r; + if (px < cx1 && py > cy2) return ((px - cx1) * (px - cx1) + (py - cy2) * (py - cy2)) <= r * r; + if (px > cx2 && py > cy2) return ((px - cx2) * (px - cx2) + (py - cy2) * (py - cy2)) <= r * r; + return 1; +} + +static int rr_border(int px, int py, int x, int y, int w, int h, int r) +{ + if (!rr_inside(px, py, x, y, w, h, r)) return 0; + int ir = r - 1; if (ir < 0) ir = 0; + if (w > 2 && h > 2 && rr_inside(px, py, x + 1, y + 1, w - 2, h - 2, ir)) return 0; + return 1; +} + +void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color) +{ + if (!g_init_ok) return; + int x1 = x, y1 = y, x2 = x + w, y2 = y + h; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + for (int py = y1; py < y2; py++) + for (int px = x1; px < x2; px++) + if (rr_inside(px, py, x, y, w, h, r)) put_px(px, py, color); + dirty_add(x1, y1, x2 - x1, y2 - y1); +} + +void meow_ui_draw_rect_round(int x, int y, int w, int h, int r, uint32_t color) +{ + if (!g_init_ok) return; + int x1 = x, y1 = y, x2 = x + w, y2 = y + h; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + for (int py = y1; py < y2; py++) + for (int px = x1; px < x2; px++) + if (rr_border(px, py, x, y, w, h, r)) put_px(px, py, color); + dirty_add(x1, y1, x2 - x1, y2 - y1); +} + +/* 圆角矩形半透明填充(M2.71-bug4):按圆角裁剪后逐像素混合 α。供「禁用变暗」等状态层用, + * 避免在圆角外的方形区域也叠色(旧 fill_rect_alpha 会在圆角外露出灰方块)。 */ +void meow_gfx_fill_round_alpha(int x, int y, int w, int h, int r, uint32_t color, int alpha) +{ + if (!g_init_ok) return; + if (alpha <= 0) return; + if (alpha >= 255) { meow_gfx_fill_round(x, y, w, h, r, color); return; } + if (w <= 0 || h <= 0) return; + int x1 = x, y1 = y, x2 = x + w, y2 = y + h; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + for (int py = y1; py < y2; py++) + for (int px = x1; px < x2; px++) + if (rr_inside(px, py, x, y, w, h, r)) put_px_blend(px, py, color, alpha); + dirty_add(x1, y1, x2 - x1, y2 - y1); +} + +/* ================= §5.8.6 batch2:圆形/线段/圆弧环(软渲,无 -lm) ================= + * 无 GPU 合成、构建只链 libz+pthread(无 -lm),故不用 math.h:COS 用象限对称查表、 + * 圆用 bbox 距离测试、弧带用「沿弧采样成粗线(圆帽)」近似。 */ +static const int m_cos90[91] = { + 1024,1024,1023,1023,1022,1020,1018,1016, + 1014,1011,1008,1005,1002,998,994,989, + 984,979,974,968,962,956,949,943, + 935,928,920,912,904,896,887,878, + 868,859,849,839,828,818,807,796, + 784,773,761,749,737,724,711,698, + 685,672,658,644,630,616,602,587, + 573,558,543,527,512,496,481,465, + 449,433,416,400,384,367,350,333, + 316,299,282,265,248,230,213,195, + 178,160,143,125,107,89,71,54, + 36,18,0, +}; + +static int m_cos_deg(int deg) /* 任意角度(可负)→ cos(deg)*1024 */ +{ + int a = deg % 360; + if (a < 0) a += 360; + if (a <= 90) return m_cos90[a]; + if (a <= 180) return -m_cos90[180 - a]; + if (a <= 270) return -m_cos90[a - 180]; + return m_cos90[360 - a]; +} +static int m_sin_deg(int deg) { return m_cos_deg(90 - deg); } + +void meow_gfx_fill_circle(int cx, int cy, int r, uint32_t color) +{ + if (!g_init_ok || r <= 0) return; + int x1 = cx - r, y1 = cy - r, x2 = cx + r + 1, y2 = cy + r + 1; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + if (x2 <= x1 || y2 <= y1) return; + int r2 = r * r; + for (int py = y1; py < y2; py++) { + int dy = py - cy; + for (int px = x1; px < x2; px++) { + int dx = px - cx; + if (dx * dx + dy * dy <= r2) put_px(px, py, color); + } + } + dirty_add(x1, y1, x2 - x1, y2 - y1); +} + +void meow_gfx_line(int x0, int y0, int x1, int y1, int th, uint32_t color) +{ + if (!g_init_ok) return; + int dx = x1 - x0, dy = y1 - y0; + int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; + int steps = (adx > ady ? adx : ady) + 1; + if (steps <= 1) { meow_gfx_fill_circle(x0, y0, th / 2 + 1, color); return; } + int cap = th / 2; + if (cap < 1) cap = 1; + for (int i = 0; i <= steps; i++) { + int x = x0 + dx * i / steps; + int y = y0 + dy * i / steps; + if (cap <= 1) put_px(x, y, color); + else meow_gfx_fill_circle(x, y, cap, color); + } +} + +void meow_gfx_ring_arc(int cx, int cy, int r, int th, int start_deg, int sweep_deg, uint32_t color) +{ + if (!g_init_ok || r <= 0) return; + int sw = sweep_deg % 360; + if (sw < 0) sw += 360; + /* 整环(|sweep|>=360):bbox 距离带(无角度判定,最快)。注意 sweep=360 → sw=0, + * 若靠 sw>=360 判整环会落下走弧分支只画一个点(顺带让 Radio 外圈/圆形进度轨道环渲染成点)。 */ + if (sweep_deg >= 360 || sweep_deg <= -360) { + int r1 = r - th / 2, r2 = r + th / 2; + if (r1 < 0) r1 = 0; + int x1 = cx - r2, y1 = cy - r2, x2 = cx + r2 + 1, y2 = cy + r2 + 1; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + if (x2 <= x1 || y2 <= y1) return; + for (int py = y1; py < y2; py++) + for (int px = x1; px < x2; px++) { + int dx = px - cx, dy = py - cy; + int d2 = dx * dx + dy * dy; + if (d2 >= r1 * r1 && d2 <= r2 * r2) put_px(px, py, color); + } + dirty_add(x1, y1, x2 - x1, y2 - y1); + return; + } + if (sw == 0) return; /* 零弧长:不画(sweep=0;整环已在上面处理) */ + /* 弧带:沿弧采样相邻点、粗线连接(圆帽)近似。方向:deg 右(0)→上(90)→左(180)→下(270),顺时针。 */ + int n = sw / 2 + 1; + if (n < 2) n = 2; + int px = cx + (m_cos_deg(start_deg) * r) / 1024; + int py = cy - (m_sin_deg(start_deg) * r) / 1024; /* 屏幕 y 向下 → sin 取负 */ + for (int i = 1; i <= n; i++) { + int deg = start_deg + sw * i / n; + int nx = cx + (m_cos_deg(deg) * r) / 1024; + int ny = cy - (m_sin_deg(deg) * r) / 1024; + meow_gfx_line(px, py, nx, ny, th, color); + px = nx; py = ny; + } +} + +void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha) +{ + if (!g_init_ok) return; + if (alpha <= 0) return; + if (alpha >= 255) { meow_ui_fill_rect(x, y, w, h, color); return; } + if (w <= 0 || h <= 0) return; + if (x < 0) { w += x; x = 0; } + if (y < 0) { h += y; y = 0; } + if (x >= g_xres || y >= g_yres) return; + if (x + w > g_xres) w = g_xres - x; + if (y + h > g_yres) h = g_yres - y; + if (w <= 0 || h <= 0) return; + if (g_clip_n > 0) { /* 绘制期裁剪:与当前 clip 求交(顶层 clip=全屏 → 无操作) */ + int *c = g_clip_stack[g_clip_n - 1]; + int x1 = x + w, y1 = y + h; + if (x < c[0]) x = c[0]; + if (y < c[1]) y = c[1]; + if (x1 > c[2]) x1 = c[2]; + if (y1 > c[3]) y1 = c[3]; + if (x1 <= x || y1 <= y) return; + w = x1 - x; h = y1 - y; + } + for (int ry = y; ry < y + h; ry++) + for (int rx = x; rx < x + w; rx++) put_px_blend(rx, ry, color, alpha); + dirty_add(x, y, w, h); +} + +/* ================= 左滑返回视觉条(GESTURE_BACK §2.3) ================= */ +#define MUI_HINT_H 5 +static int g_hint_last = 0; /* 上次画过的条长(清条时只清到它,避免残留) */ + +/* 顶部条区「快照恢复」:画条前把 [0..宽)×[hy..hy+MUI_HINT_H) 拍进小缓冲区;回缩/清空时 + * 回填快照还原「真实底色」而非 th->bg —— 全屏页面底=主题 accent,若填 th->bg(黑) 会在 + * accent 底上留下黑色残条(直到下次区域刷新才消失)。快照只在首现条时拍一次(后续 grow + * 不再覆盖快照),每行/全长布局全保留;缓冲为一次性分配(宽×H 行,480×5×2B≈4.8KB)。 */ +static unsigned char *g_hint_snap = NULL; /* 快照(g_line×MUI_HINT_H 字节;NULL=未拍/分配失败) */ +static int g_hint_snap_y = 0; /* 快照行的起始 y(本次手势时的 hy) */ + +/* 拍下「顶部条区」当前 back_buf 内容(只在 g_hint_last==0 的首现条时调用)。 */ +static void hint_snapshot(void) +{ + int hy = meow_ui_statusbar_height(); + if (hy < 0) hy = 0; + g_hint_snap_y = hy; + if (hy + MUI_HINT_H > g_yres) return; /* 越界:放弃(回缩走 bg 兜底) */ + size_t len = (size_t)MUI_HINT_H * (size_t)g_line; + if (!g_hint_snap) { + g_hint_snap = malloc(len); + if (!g_hint_snap) return; + } + memcpy(g_hint_snap, g_fb + (size_t)hy * g_line, len); +} + +/* 从快照回填列区间 [x0,x1) 的 MUI_HINT_H 行(还原条下真实底色)。x0/x1 已夹到屏宽。 */ +static void hint_restore(int x0, int x1) +{ + if (x1 <= x0) return; + if (x0 < 0) x0 = 0; + if (x1 > g_xres) x1 = g_xres; + if (!g_hint_snap) { /* 无快照(分配失败):兜底填背景(旧行为) */ + const meow_theme *th = meow_theme_get(); + meow_ui_fill_rect(x0, g_hint_snap_y, x1 - x0, MUI_HINT_H, th->bg); + return; + } + int bpp = g_bpp / 8; + for (int r = 0; r < MUI_HINT_H; r++) { + size_t row = (size_t)(g_hint_snap_y + r) * g_line; + memcpy(g_fb + row + (size_t)x0 * bpp, + g_hint_snap + (size_t)r * g_line + (size_t)x0 * bpp, + (size_t)(x1 - x0) * bpp); + } + meow_gfx_mark_dirty(x0, g_hint_snap_y, x1 - x0, MUI_HINT_H); /* 标脏:调用方 flush_region 上屏 */ +} + +void meow_ui_swipe_hint(int x) +{ + if (!g_init_ok) return; + const meow_theme *th = meow_theme_get(); + /* 进度条画在状态栏下沿(z 在状态栏之下,不被全局状态栏覆盖)。 + * 有状态栏(非全屏)→ y = 状态栏高度;收合(collapsed)→ y=提示线下缘 + * (MUI_SBAR_COLLAPSE_H,与提示线不重叠);无状态栏(全屏/隐藏)→ y=0。 */ + int hy = meow_ui_statusbar_height(); + /* 单一实现(与非全屏 Hello 完全一致,全屏仅 hy 不同):grow 画 0..x、shrunk 先把 + * 超出部分还原为快照、x<=0 整条还原。x<0 视为清空;条长上限=屏宽。条色仍取 th->fg。 */ + if (x > 0) { + if (x > g_xres) x = g_xres; + if (g_hint_last == 0) hint_snapshot(); /* 首现条:拍下未污染区,供此后回填 */ + /* 往回滑(新长度 < 上次)时,把「上次画到旧长度、本次超出的 [x,old)」还原为快照 + * 真实底色(全屏 accent),而非填 th->bg —— 否则左滑回缩在 accent 底上漏黑。 */ + if (g_hint_last > x) hint_restore(x, g_hint_last); + meow_ui_fill_rect(0, hy, x, MUI_HINT_H, th->fg); + g_hint_last = x; + } else if (g_hint_last > 0) { + /* 手指回原点/抬手未达标/抬手达标:整条还原为快照真实底色(全屏 accent 上无黑残) */ + hint_restore(0, g_hint_last); + g_hint_last = 0; + } + /* 不发 flush:由调用方 meow_ui_flush_region(0,0,宽,6) 只刷顶部条 */ +} + +/* ================= 5x7 回退点阵字(与 launcher/hello 一致的表,独立携带) ================= */ +static const unsigned char mfont5x7[][8] = { + {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E,0x00},{0x04,0x0C,0x04,0x04,0x04,0x04,0x0E,0x00}, + {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F,0x00},{0x1F,0x02,0x04,0x02,0x01,0x11,0x0E,0x00}, + {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02,0x00},{0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E,0x00}, + {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E,0x00},{0x1F,0x01,0x02,0x04,0x08,0x08,0x08,0x00}, + {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E,0x00},{0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C,0x00}, + {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11,0x00},{0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E,0x00}, + {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E,0x00},{0x1C,0x12,0x11,0x11,0x11,0x12,0x1C,0x00}, + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F,0x00},{0x1F,0x10,0x10,0x1E,0x10,0x10,0x10,0x00}, + {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F,0x00},{0x11,0x11,0x11,0x1F,0x11,0x11,0x11,0x00}, + {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E,0x00},{0x07,0x02,0x02,0x02,0x02,0x12,0x0C,0x00}, + {0x11,0x12,0x14,0x18,0x14,0x12,0x11,0x00},{0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x00}, + {0x11,0x1B,0x15,0x15,0x11,0x11,0x11,0x00},{0x11,0x19,0x15,0x13,0x11,0x11,0x11,0x00}, + {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},{0x1E,0x11,0x11,0x1E,0x10,0x10,0x10,0x00}, + {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D,0x00},{0x1E,0x11,0x11,0x1E,0x14,0x12,0x11,0x00}, + {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E,0x00},{0x1F,0x04,0x04,0x04,0x04,0x04,0x04,0x00}, + {0x11,0x11,0x11,0x11,0x11,0x11,0x0E,0x00},{0x11,0x11,0x11,0x11,0x11,0x0A,0x04,0x00}, + {0x11,0x11,0x11,0x15,0x15,0x1B,0x11,0x00},{0x11,0x11,0x0A,0x04,0x0A,0x11,0x11,0x00}, + {0x11,0x11,0x11,0x0A,0x04,0x04,0x04,0x00},{0x1F,0x01,0x02,0x04,0x08,0x10,0x1F,0x00}, + {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x00,0x00,0x00,0x00,0x04,0x00}, + {0x04,0x04,0x04,0x04,0x04,0x00,0x04,0x00},{0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, + {0x00,0x00,0x00,0x1F,0x00,0x00,0x00,0x00}, +}; +#define FONT5X7_N (sizeof(mfont5x7)/sizeof(mfont5x7[0])) + +static int glyph_index(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'Z') return 10 + (c - 'A'); + if (c >= 'a' && c <= 'z') return 10 + (c - 'a'); + if (c == ' ') return 36; + if (c == '.') return 37; + if (c == '!') return 38; + if (c == '-' || c == '_') return 39; + return 36; +} + +static void draw_glyph5x7(int x, int y, char c, int scale, uint32_t color) +{ + const unsigned char *g = mfont5x7[glyph_index(c)]; + for (int row = 0; row < 7; row++) { + unsigned char bits = g[row]; + for (int col = 0; col < 5; col++) { + if (bits & (0x10 >> col)) { + for (int dy = 0; dy < scale; dy++) + for (int dx = 0; dx < scale; dx++) + put_px(x + col * scale + dx, y + row * scale + dy, color); + } + } + } +} + +static void draw_text5x7(int x, int y, const char *s, int scale, uint32_t color) +{ + while (*s) { draw_glyph5x7(x, y, *s, scale, color); x += 6 * scale; s++; } +} + +/* ================= 文本(字库优先,回退点阵) ================= */ +static int fallback_scale(int size) { return (size >= 16) ? 2 : 1; } + +static void text_draw(int x, int y, const char *s, int size, uint32_t color) +{ + if (!g_init_ok || !s) return; + if (g_mfont_ready && g_bpp == 16) { + int w = meow_font_width_text(s, size); + int hh = meow_font_line_height(size); + if (g_clip_n > 0) { + int *c = g_clip_stack[g_clip_n - 1]; + if (x + w <= c[0] || x >= c[2] || y + hh <= c[1] || y >= c[3]) return; /* 与 clip 无交 */ + if (!(x >= c[0] && x + w <= c[2] && y >= c[1] && y + hh <= c[3])) { + /* 部分相交:用「偏移 fb 子矩形 + 临时 fb 几何」做裁剪(meow_font 只按全屏剪)。 + * 把 fb 基址指向 clip 左上角、fb_w/h 设为 clip 尺寸、原点平移到 clip 坐标, + * 这样 meow_font 的逐字形裁剪天然落在 clip 内;画完恢复全屏几何。 */ + uint16_t *fb = (uint16_t *)(g_fb + (size_t)c[1] * g_line + (size_t)c[0] * 2); + meow_font_set_fb((unsigned)(c[2] - c[0]), (unsigned)(c[3] - c[1]), (unsigned)g_line); + meow_font_drawtext(fb, x - c[0], y - c[1], s, rgb565(color), size); + meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); + dirty_add(x, y, w, hh); + return; + } + } + meow_font_drawtext((uint16_t *)g_fb, x, y, s, rgb565(color), size); + dirty_add(x, y, w, hh); + } else { + int sc = fallback_scale(size); + draw_text5x7(x, y, s, sc, color); /* 走 put_px,自动过 clip */ + dirty_add(x, y, (int)strlen(s) * 6 * sc, 7 * sc); + } +} + +void meow_ui_text(int x, int y, const char *utf8, int size, uint32_t color) +{ text_draw(x, y, utf8, size, color); } + +void meow_ui_text_right(int right_x, int y, const char *utf8, int size, uint32_t color) +{ text_draw(right_x - meow_ui_text_width(utf8, size), y, utf8, size, color); } + +void meow_ui_text_center(int cx, int y, const char *utf8, int size, uint32_t color) +{ text_draw(cx - meow_ui_text_width(utf8, size) / 2, y, utf8, size, color); } + +int meow_ui_text_width(const char *utf8, int size) +{ + if (!utf8) return 0; + if (g_mfont_ready) return meow_font_width_text(utf8, size); + return (int)strlen(utf8) * 6 * fallback_scale(size); +} + +int meow_ui_text_height(int size) +{ + if (g_mfont_ready) return meow_font_line_height(size); + return 7 * fallback_scale(size); +} + +/* ================= RGBA blit(图片层用) ================= */ +void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha) +{ + if (!g_init_ok || !rgba || w <= 0 || h <= 0 || alpha <= 0) return; + int x1 = x, y1 = y, x2 = x + w, y2 = y + h; + if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; + if (x2 > g_xres) x2 = g_xres; if (y2 > g_yres) y2 = g_yres; + for (int py = y1; py < y2; py++) { + int sy = py - y; + for (int px = x1; px < x2; px++) { + int sx = px - x; + const uint8_t *p = rgba + ((size_t)sy * w + sx) * 4; + int a = (alpha * p[3]) / 255; + if (a <= 0) continue; + uint32_t c = ((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]; + if (a >= 255) put_px(px, py, c); + else put_px_blend(px, py, c, a); + } + } + dirty_add(x1, y1, x2 - x1, y2 - y1); +} + +/* ================= present / 上屏 ================= */ +static void dump_ppm(void) +{ + if (!g_dump_path || !g_back_buf) return; + FILE *f = fopen(g_dump_path, "wb"); + if (!f) return; + fprintf(f, "P6\n%d %d\n255\n", g_xres, g_yres); + for (int y = 0; y < g_yres; y++) { + for (int x = 0; x < g_xres; x++) { + unsigned char r, g, b; + if (g_bpp == 16) { + uint16_t px = *(uint16_t *)(g_back_buf + (size_t)y * g_line + (size_t)x * 2); + r = ex5((px >> 11) & 0x1F); g = ex6((px >> 5) & 0x3F); b = ex5(px & 0x1F); + } else { + uint32_t px = *(uint32_t *)(g_back_buf + (size_t)y * g_line + (size_t)x * 4); + r = (px >> 16) & 0xFF; g = (px >> 8) & 0xFF; b = px & 0xFF; + } + fputc(r, f); fputc(g, f); fputc(b, f); + } + } + fclose(f); +} + +/* ================= 状态栏「上收出屏只留提示线」(collapsed,基座) ================= */ +/* 每帧推进上收/恢复:向目标偏移(collapsed→MUI_SBAR_H、否则→0)线性逼近; + * 无过渡时长(g_sbar_collapse_anim_ms<=0)= 一步到位(基座默认)。动画插值骨架在此, + * 后续代理可换缓动/每帧步进。 */ +static void statusbar_collapse_step(void) +{ + int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0; + if (g_sbar_collapse_h == tgt) return; + if (g_sbar_collapse_anim_ms <= 0) { g_sbar_collapse_h = tgt; return; } + unsigned long long now = now_ms(); + unsigned long long el = now - g_sbar_collapse_start_ms; + int span = g_sbar_collapse_anim_ms; + if ((int)el >= span) { g_sbar_collapse_h = tgt; return; } + int delta = tgt - g_sbar_collapse_from_h; + g_sbar_collapse_h = g_sbar_collapse_from_h + + (int)((long long)delta * (long long)el / (long long)span); +} + +/* 上收中是否需要逐帧重画(动画未达目标位置时置位,强制每帧推进)。 */ +static int statusbar_collapse_in_transition(void) +{ + int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0; + return g_sbar_collapse_h != tgt; +} + +/* 上收状态栏:内容整体上移出屏(不画时钟/电量),仅在顶部画一条「提示线」。 + * 与 meow_ui_swipe_hint(左滑返回进度条,5px,动态)不同:此线是「状态栏可下拉还原」的 + * 常驻提示(主题强调色下边缘)。收合只占/清「提示线」高度区域(MUI_SBAR_COLLAPSE_H=4), + * 不再清整 MUI_SBAR_H(28) 行 —— app 内容按 inset=提示线高可上移到提示线下缘,无 28px 空条。 */ +static void statusbar_collapsed_render(int h) +{ + (void)h; + const meow_theme *th = meow_theme_get(); + int w = meow_ui_width(); + /* h 越大内容越贴顶;基座:折叠态清「提示线」区域底 + 顶部提示线(提示线下缘=主题 accent) */ + meow_ui_fill_rect(0, 0, w, MUI_SBAR_COLLAPSE_H, th->bg); + meow_ui_fill_rect(0, MUI_SBAR_COLLAPSE_H - 1, w, 1, th->accent); +} + +/* ---- 公共 API(基座实现;动画插值骨架见 statusbar_collapse_step)---- */ +void meow_ui_statusbar_set_collapsed(int on) +{ + int tgt = on ? 1 : 0; + if (tgt == g_sbar_collapsed) return; + g_sbar_collapsed = tgt; + g_sbar_collapse_from_h = g_sbar_collapse_h; + g_sbar_collapse_start_ms = now_ms(); + dirty_full(); /* 顶栏显隐/上收变化 → 全帧重画 */ + meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合,agent-146) */ +} + +int meow_ui_statusbar_collapsed(void) { return g_sbar_collapsed; } + +/* 当前上收进度(0..MUI_SBAR_H)。供 launcher 按「可见条高 = 28-offset」标脏切齐(agent-146)。 */ +int meow_ui_statusbar_collapse_offset(void) { return g_sbar_collapse_h; } + +/* 上收/恢复过渡是否仍在推进(current offset != target)。供 launcher 过渡期高频刷(agent-146)。 */ +int meow_ui_statusbar_animating(void) +{ + int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0; + return g_sbar_collapse_h != tgt; +} + +void meow_ui_statusbar_animate(int ms) +{ + g_sbar_collapse_anim_ms = (ms > 0) ? ms : 0; /* 0=瞬切(基座默认) */ +} + +int meow_ui_statusbar_hotzone(void) +{ + /* 通知中心触发(顶部下滑)热区:与渲染分离(agent-146 定案:收合 ≠ 禁用顶部热区)。 + * 状态栏「激活」(非全屏隐藏,含收合)时热区恒 = 整条状态栏高 MUI_SBAR_H(28)——即使收合 + * 也只上收渲染,顶部下滑仍可打开通知中心(渲染与热区事件分离)。 + * 仅「全屏隐藏」(immersive,inset0)时缩小到 MEOW_SBAR_HOTZONE_FS(保留一处下拉入口)。 */ + if (meow_ui_sys_statusbar_active()) + return MUI_SBAR_H; + return MEOW_SBAR_HOTZONE_FS; +} + +/* 顶部状态栏条是否被本次脏区触及(含整帧):触及或数据已变(force)才需要重画顶条。 */ +static int statusbar_touched(void) +{ + if (g_dirty_full) return 1; + for (int i = 0; i < g_ndirty; i++) + if (g_dirty[i][1] < MUI_SBAR_H) return 1; /* 脏矩形与顶条(前 MUI_SBAR_H 行)相交 */ + return 0; +} + +/* ---- §10.5 叠加让位自抑(present 层)---- + * /tmp/meow_notif_active 存在 = 通知中心叠加会话进行中(launcher 正在 fb 上渲染面板)。 + * App 进程(非系统 UI 渲染者)此时不上屏;200ms 节流缓存存在性(别每帧 stat)。 + * 计数 g_overlay_suppressed 供宿主单测断言(meow_gfx_overlay_suppressed_count)。 */ +static unsigned long long g_overlay_suppressed = 0; + +static int overlay_present_suppressed(void) +{ + if (meow_ui_sys_render_enabled()) return 0; /* launcher(系统 UI)渲染面板,照常上屏 */ + 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 : MEOW_NOTIF_ACTIVE_DEFAULT); + s_path_init = 1; + } + unsigned long long now = now_ms(); + if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res; + s_last_chk = now; + s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0; + return s_last_res; +} + +unsigned long long meow_gfx_overlay_suppressed_count(void) { return g_overlay_suppressed; } + +static void present(void) +{ + if (!g_init_ok || !g_back_buf) return; + if (!(g_dirty_full || g_ndirty > 0)) return; + /* §10.5 叠加让位自抑(仅 App 进程):App 前台播放中唤起通知中心时 launcher 不 SIGSTOP + * (保 PCM 供流,ncm 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf, + * 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层。系统 UI 进程(launcher 渲染面板) + * 不抑制。抑制期脏区保留累积(超上限自然升级整帧),标记消失后下一次 present 一次上屏。 */ + if (overlay_present_suppressed()) { g_overlay_suppressed++; return; } + /* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现; + * 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */ + unsigned long long now = now_ms(); + int period = (g_eff_rate > 0) ? (1000 / g_eff_rate) : 16; + if (!g_refresh_always_max) { + if (g_last_present_ms && now - g_last_present_ms < (unsigned long long)period) return; + } + /* §6.1 ②:App 跨 Activity 全局控件(SDK 托管骨架)。在 active activity 内容之上、 + * 全局状态栏之下绘制——跨 Activity 常驻,仅在有帧需上屏(已过帧率门控)时调用。 + * render_cb 内的 meow_ui_* 绘制会自行标脏,随下方 dirty 列表一并上屏。 */ + meow_ui_global_render(); + /* 全局状态栏(A-0)+ 上收(collapsed)基座:上屏前把状态栏画进 back_buf 顶部行。 + * 展开态=整条(MUI_SBAR_H 行);收合态只占/清「提示线」区域(MUI_SBAR_COLLAPSE_H 行), + * app 内容(inset=提示线高)可上移到提示线下缘,无 28px 空条。全屏/隐藏时 active()==0 + * 不画(沉浸),app 内容可占满整屏。触发条件:数据已变(1s 轻刷/显隐切换) / + * 顶条被 app 内容触及 / 上收动画未到位(逐帧推进)。展开态仍走 + * meow_ui_sys_statusbar_render()(meow_ui_sys.c 不改,留给后续代理); + * 上收折叠态(collapse_h>=MUI_SBAR_H)由基座只画提示线(内容上移出屏)。 */ + if (meow_ui_sys_render_enabled() && meow_ui_sys_statusbar_active() && + (meow_ui_sys_statusbar_force() || statusbar_touched() || + statusbar_collapse_in_transition())) { + statusbar_collapse_step(); + if (g_sbar_collapse_h >= MUI_SBAR_H) + statusbar_collapsed_render(g_sbar_collapse_h); + else + /* 收合过渡/展开态:内容上移 g_sbar_collapse_h 像素并裁顶(yoff=0=整条)。 + * 仅画可见条高 [0, 28-offset) 行,其余行=App 内容(launcher 按 statusbar_height 标脏 + * 切齐),实现「平滑上移出屏」而不残留、不黑框。 */ + meow_ui_sys_statusbar_render_off(g_sbar_collapse_h); + } + if (g_fb_mem) { + ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */ + unsigned long long tbg = now_ms(); + /* 「同一个状态栏」闭环:App 进程不渲染状态栏(meow_ui_sys_render_enabled()==0),也绝不向 + * 真实 fb 写顶条 inset 区(顶条由 launcher 系统进程叠加绘制)。否则 App 首帧全量拷贝 + * (meow_ui_init 置 dirty_full)会把 launcher 刚叠画的顶条冲成未初始化数据——真机 + * 「进入 App 顶条黑 1 秒再出现」。sb_clip = 本进程当前状态栏 inset(展开=28 / 收合=4 / + * 隐藏=0):全量拷贝只拷 [sb, yres) 行;逐 dirty 矩形把上沿截断到 y>=sb,整块落在顶条区则跳过。 + * 仅 App 进程(非系统 UI)截断;launcher(系统 UI)sb_clip=0,行为不变。 */ + int sb_clip = 0; + if (!meow_ui_sys_render_enabled()) sb_clip = meow_ui_statusbar_height(); + if (sb_clip < 0) sb_clip = 0; + if (sb_clip > g_yres) sb_clip = g_yres; + if (g_dirty_full) { + if (sb_clip > 0) { + memcpy(g_fb_mem + (size_t)sb_clip * g_line, + g_back_buf + (size_t)sb_clip * g_line, + (size_t)g_line * (size_t)(g_yres - sb_clip)); + } else { + memcpy(g_fb_mem, g_back_buf, (size_t)g_line * (size_t)g_yres); + } + } else { + int bppb = g_bpp / 8; + for (int i = 0; i < g_ndirty; i++) { + int rw = g_dirty[i][2], ry = g_dirty[i][1], rh = g_dirty[i][3]; + if (sb_clip > 0) { + if (ry + rh <= sb_clip) continue; /* 整块在顶条 inset 区:App 不写 */ + if (ry < sb_clip) { /* 上沿与顶条区相交:截断到 sb_clip */ + rh -= (sb_clip - ry); + ry = sb_clip; + if (rh <= 0 || rw <= 0) continue; + } + } + for (int yy = 0; yy < rh; yy++) { + size_t off = (size_t)(ry + yy) * g_line + (size_t)g_dirty[i][0] * bppb; + memcpy(g_fb_mem + off, g_back_buf + off, (size_t)rw * bppb); + } + } + } + refresh_governor(now_ms() - tbg, period); + } + g_last_present_ms = now; + g_frames_presented++; + if (g_dump_path) dump_ppm(); + dirty_reset(); +} + +void meow_ui_flush(void) { if (!g_flush_hold) present(); } + +void meow_ui_flush_region(int x, int y, int w, int h) +{ dirty_add(x, y, w, h); if (!g_flush_hold) present(); } + +/* ---- 帧率调度设置接口(launcher 与 app 共用;归口 PERF_SCHEDULER 显示刷新率子域)---- */ +/* 设目标档位(60/30/15):同时把当前有效档对齐到目标(与 launcher settings_load 语义一致)。 */ +void meow_ui_set_refresh_rate(int r) +{ + if (r != 60 && r != 30 && r != 15) return; + g_refresh_rate_cfg = r; + g_eff_rate = r; +} + +/* 1=恒 max 不降档(性能优先);0=允许 governor 在 [15,30,60] 内自适应降/升档。 */ +void meow_ui_set_refresh_always_max(int on) { g_refresh_always_max = on ? 1 : 0; } + +/* 当前有效帧率(governor 可能已降档;验证/诊断用)。 */ +int meow_ui_effective_rate(void) { return g_eff_rate; } + +/* 已呈现帧计数(验证 present 频率用)。 */ +unsigned long long meow_ui_frames_presented(void) { return g_frames_presented; } + +int meow_gfx_is_device(void) { return g_fb_mem != NULL; } +int meow_gfx_mfont_ready(void) { return g_mfont_ready; } + +/* ---- 字体档位(公共 API,基座定义 + font 域真重载钩子)---- + * meow_font_tier()/meow_font_set_tier() 在 meow_ui_gfx.c 定义(符号唯一),font 代理勿在 + * meow_font.c 重复定义;大字库按档装载由 meow_font.c meow_font_reload_tier() 实现,并在 + * meow_font_set_tier() 处唤起(见下)。 */ +int meow_font_tier(void) { return g_font_tier; } + +int meow_font_set_tier(int tier) +{ + int old = g_font_tier; + if (tier < MEOW_FONT_TIER_COMMON) tier = MEOW_FONT_TIER_COMMON; + if (tier > MEOW_FONT_TIER_FULL) tier = MEOW_FONT_TIER_FULL; + if (tier == g_font_tier) return g_font_tier; + g_font_tier = tier; + /* 真重载(font 域钩子,M2.5x):按档装载大字库。launcher 已按档解析并注入 + * MEOW_FONT_PNG/IDX(tier>0 → GBK/Full 大字库绝对路径);重读 env 重载。 + * 无 env/失败 → 回退卡默认 MeowOS/fonts/default → 固件。 + * 大字库 atlas 大(GBK≈5-15MB+),meow_font_reload_tier 先 unload 旧再 load 新,防双驻留。 */ + const char *ep = getenv("MEOW_FONT_PNG"); + const char *ei = getenv("MEOW_FONT_IDX"); + if (ep && ep[0] && ei && ei[0] && meow_font_reload_tier(tier, ep, ei) == 0) + return g_font_tier; + if (meow_font_reload_tier(tier, MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) + return g_font_tier; + if (meow_font_reload_tier(tier, MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) + return g_font_tier; + g_font_tier = old; /* 全部失败 → 回退旧档(字库可能已不可用,5x7 点阵兜底,系统不死) */ + return g_font_tier; +} + +/* ================= 初始化/销毁 ================= */ +/* 字库加载优先级(M2.x): + * 1) 环境变量 MEOW_FONT_PNG/MEOW_FONT_IDX(launcher 注入的「当前生效字体」= 系统 font.file) + * —— app 未自带字库(§5.7 load_handle 未实现)时跟随系统 font.file; + * 2) 卡默认 MeowOS/fonts/default/regular/font.png(M2.6 字重结构;不再硬编码 MeowAPPs/fonts); + * 3) 固件 /usr/share/meowos/font/font.png。 + * 全失败 → g_mfont_ready=0,text_draw 回退 5x7 点阵(系统不死)。 */ +static int load_font(void) +{ + /* 字体档位(MEOW_FONT_TIER env,由 launcher 按 app.json "fonts" 注入): + * 0=默认4K常用 1=大字库GBK 2=Full。launcher 已按档解析大字库路径并经 MEOW_FONT_PNG/IDX + * 注入(见 meow_font_effective_paths_tiered),故下方 meow_font_load(ep,ei) 即装载 + * 对应档位字库(tier>0 → GBK/Full 大字库);此处 read env 存档对齐公共 get/set。 */ + const char *etr = getenv("MEOW_FONT_TIER"); + if (etr && etr[0]) { + g_font_tier = atoi(etr); + if (g_font_tier < MEOW_FONT_TIER_COMMON) g_font_tier = MEOW_FONT_TIER_COMMON; + if (g_font_tier > MEOW_FONT_TIER_FULL) g_font_tier = MEOW_FONT_TIER_FULL; + } + const char *ep = getenv("MEOW_FONT_PNG"); + const char *ei = getenv("MEOW_FONT_IDX"); + if (ep && ep[0] && ei && ei[0] && meow_font_load(ep, ei) == 0) return 1; + if (meow_font_load(MEOW_FONT_PNG_CARD, MEOW_FONT_IDX_CARD) == 0) return 1; + if (meow_font_load(MEOW_FONT_PNG_SYS, MEOW_FONT_IDX_SYS) == 0) return 1; + return 0; +} + +/* M2.5x 多字体:读 launcher 注入的环境变量,构建 6 字符类 → 字库的映射(同 family 去重、 + * ≤4 套上限由 meow_font_config_multifont 统一处理)。launcher 注入(见 meow_launcherd/app_launcher.c + * `launch_app`):MEOW_MULTI_FONT=0/1;MEOW_FONT_PNG/IDX=全局(保留);每类 + * MEOW_FONT_CLS__PNG/IDX(命中则覆盖全局;未注入 = 该类跟随全局)。 + * 仅当全局字库已装载(g_mfont_ready)才配置;关/无 env → 单字体,零额外开销。 */ +static void meow_gfx_load_multifont(void) +{ + static const char *cls_env[MEOW_FCLS_COUNT][2] = { + { "MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_IDX" }, + { "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_CJK_SYM_IDX" }, + { "MEOW_FONT_CLS_NUM_PNG", "MEOW_FONT_CLS_NUM_IDX" }, + { "MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_IDX" }, + { "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_LATIN_SYM_IDX" }, + { "MEOW_FONT_CLS_OTHER_PNG", "MEOW_FONT_CLS_OTHER_IDX" }, + }; + const char *emf = getenv("MEOW_MULTI_FONT"); + int on = emf && emf[0] == '1'; + const char *gp = getenv("MEOW_FONT_PNG"); + const char *gi = getenv("MEOW_FONT_IDX"); + meow_font_class_cfg cfg[MEOW_FCLS_COUNT]; + memset(cfg, 0, sizeof cfg); + for (int c = 0; c < MEOW_FCLS_COUNT; c++) { + const char *p = getenv(cls_env[c][0]); + const char *idx = getenv(cls_env[c][1]); + if (p && p[0] && idx && idx[0]) { cfg[c].png = p; cfg[c].idx = idx; } + } + int distinct = 0, truncated = 0; + if (meow_font_config_multifont(on, gp, gi, cfg, &distinct, &truncated) != 0) + meow_log(MEOW_LOG_WARN, "multi_font: config failed, single-font fallback"); + else if (truncated) + meow_log(MEOW_LOG_WARN, "multi_font: >%d distinct fonts -> capped (more memory!)", distinct); +} + +int meow_ui_init(const meow_ui_opts *opts) +{ + if (g_init_ok) return 0; + meow_log_install_exit_handler(); /* M2.78:SIGTERM/SIGINT 兜底写 exit: 日志 */ + meow_ui_install_sigusr1_guard(); /* M2.87:SIGUSR1 无害护栏(防泄漏 SIGUSR1 杀 App) */ + int fw = 0, fh = 0; + if (opts) { fw = opts->width; fh = opts->height; } + + int fd = open(FB_DEV, O_RDWR); + if (fd >= 0) { + unsigned char vinfo[160], finfo[68]; + memset(vinfo, 0, sizeof vinfo); + memset(finfo, 0, sizeof finfo); + if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) == 0 && + ioctl(fd, FBIOGET_FSCREENINFO, finfo) == 0) { + g_xres = *(uint32_t *)(vinfo + 0); + g_yres = *(uint32_t *)(vinfo + 4); + g_bpp = *(uint32_t *)(vinfo + 24); + g_line = *(uint32_t *)(finfo + 44); + if (!g_line) g_line = g_xres * (g_bpp / 8); + if (g_bpp != 16 && g_bpp != 32) g_bpp = 16; + size_t maplen = (size_t)g_line * (size_t)g_yres; + void *m = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (m != MAP_FAILED) { g_fb_mem = m; g_fb_fd = fd; } + else { close(fd); fd = -1; g_fb_mem = NULL; } + } else { close(fd); fd = -1; g_fb_mem = NULL; } + } + + /* host/qemu 无 fb:使用缺省/覆写几何,走离屏(present 只 dump) */ + if (g_fb_mem == NULL) { + if (g_xres <= 0 || g_xres > 4096) g_xres = MUI_DEFAULT_W; + if (g_yres <= 0 || g_yres > 4096) g_yres = MUI_DEFAULT_H; + if (g_bpp != 16 && g_bpp != 32) g_bpp = 16; + g_line = g_xres * (g_bpp / 8); + if (fd >= 0) { close(fd); fd = -1; } + } + if (fw > 0 && fw <= 4096) g_xres = fw; + if (fh > 0 && fh <= 4096) g_yres = fh; + g_line = g_xres * (g_bpp / 8); + + g_back_buf = malloc((size_t)g_line * (size_t)g_yres); + if (!g_back_buf) { if (g_fb_mem) munmap(g_fb_mem, 0); if (fd >= 0) close(fd); return -1; } + g_fb = g_back_buf; + + meow_font_set_fb((unsigned)g_xres, (unsigned)g_yres, (unsigned)g_line); + g_mfont_ready = load_font(); + if (g_mfont_ready) meow_gfx_load_multifont(); /* M2.5x 多字体(读 env;关/无 env = 单字体) */ + + const char *dp = getenv("MEOW_UI_DUMP"); + if (dp && *dp) g_dump_path = strdup(dp); + + g_init_ok = 1; + dirty_full(); + meow_ui_sys_init(); /* A-0:系统状态读取器首刷(电量/WiFi/蓝牙/音量),状态栏就绪 */ + /* 全屏预置(基座):launcher 按 app.json "fullscreen" 注入 MEOW_FULLSCREEN=1,SDK 据此 + * 预先设全屏(隐藏整条状态栏/沉浸,statusbar_active()==0、statusbar_height()==0)。 + * 未注入/非"1"=非全屏(默认)。放在 sys_init 后使首帧即为全屏态。 */ + const char *efs = getenv("MEOW_FULLSCREEN"); + if (efs && efs[0] == '1') meow_ui_set_fullscreen(1); + return 0; +} + +void meow_ui_shutdown(void) +{ + if (!g_init_ok) return; + meow_font_unload(); + if (g_fb_mem) munmap(g_fb_mem, (size_t)g_line * (size_t)g_yres); + if (g_fb_fd >= 0) close(g_fb_fd); + if (g_back_buf) free(g_back_buf); + if (g_dump_path) { free(g_dump_path); g_dump_path = NULL; } + if (g_hint_snap) { free(g_hint_snap); g_hint_snap = NULL; } + g_back_buf = NULL; g_fb = NULL; g_fb_mem = NULL; + g_fb_fd = -1; g_init_ok = 0; g_mfont_ready = 0; + dirty_reset(); +} + +int meow_ui_width(void) { return g_xres; } +int meow_ui_height(void) { return g_yres; } diff --git a/sdk/meow_ui_global.c b/sdk/meow_ui_global.c new file mode 100644 index 0000000..d91dc20 --- /dev/null +++ b/sdk/meow_ui_global.c @@ -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; +} diff --git a/sdk/meow_ui_ime.c b/sdk/meow_ui_ime.c new file mode 100644 index 0000000..b28ad6e --- /dev/null +++ b/sdk/meow_ui_ime.c @@ -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 +#include +#include +#include +#include + +/* 阻塞 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= 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= 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; +} diff --git a/sdk/meow_ui_img.c b/sdk/meow_ui_img.c new file mode 100644 index 0000000..c2bd08b --- /dev/null +++ b/sdk/meow_ui_img.c @@ -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 +#include +#include + +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); +} diff --git a/sdk/meow_ui_input.c b/sdk/meow_ui_input.c new file mode 100644 index 0000000..e105f5a --- /dev/null +++ b/sdk/meow_ui_input.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 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_dxt_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(); +} diff --git a/sdk/meow_ui_internal.h b/sdk/meow_ui_internal.h new file mode 100644 index 0000000..c1f895a --- /dev/null +++ b/sdk/meow_ui_internal.h @@ -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 +#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 */ diff --git a/sdk/meow_ui_log.c b/sdk/meow_ui_log.c new file mode 100644 index 0000000..3817ea8 --- /dev/null +++ b/sdk/meow_ui_log.c @@ -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 分层)、 + * MEOW_LOG_APP(app_id,无则 "app")、MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写 + * `/<启动时间>_[._].log`(目录自动 mkdir -p), + * 按 app 分目录 + 每次启动新文件(不同启动=不同文件,同一次启动内 O_APPEND 追加), + * 级别过滤(lvl < level 丢弃); + * - 格式:` [] message\n`(meow_ui_time_ms() 取毫秒); + * - 无 log_dir(无卡)/目录不可建/写失败 → 降级 stderr,不阻塞 app 不崩。 + * + * 上限(用户定,防撑爆 TF 卡): + * - 单文件 ≥ MEOW_LOG_FILE_MAX(2MB)→ 关当前 fd,开新文件并追加序号 `_`; + * - 每 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* 单行消息上限(超长截断) */ +#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`;M2.x 简化后 MEOW_LOG_DIR 已含 app 分层 + * (launcher 注入 logs/)。g_log_base 存不含序号后缀的路径前缀 `/_`; + * g_log_seq 为轮转序号(0=首个文件无后缀;>0 追加 `_`)。两者均在 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 追加 _)。 */ +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:构建完整前缀 `/_`(M2.x 简化:MEOW_LOG_DIR 已含 app 分层)。 + * ts 用 localtime 的 %Y%m%d_%H%M%S(启动时刻);尾拼 _ 防同秒重复启动撞名, + * 从而每次启动=新文件,同一次启动内 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:(绕过 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:对 ``(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 总量清理:扫描 `/`(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; + /* 单文件超限 → 轮转开新文件(_ 递增);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; +} diff --git a/sdk/meow_ui_media.c b/sdk/meow_ui_media.c new file mode 100644 index 0000000..59c54a8 --- /dev/null +++ b/sdk/meow_ui_media.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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; +} diff --git a/sdk/meow_ui_media.h b/sdk/meow_ui_media.h new file mode 100644 index 0000000..20394f3 --- /dev/null +++ b/sdk/meow_ui_media.h @@ -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 + +#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 */ diff --git a/sdk/meow_ui_nav.c b/sdk/meow_ui_nav.c new file mode 100644 index 0000000..69897b4 --- /dev/null +++ b/sdk/meow_ui_nav.c @@ -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 +#include +#include + +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; +} diff --git a/sdk/meow_ui_nav.h b/sdk/meow_ui_nav.h new file mode 100644 index 0000000..d786588 --- /dev/null +++ b/sdk/meow_ui_nav.h @@ -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= / MEOW_CHAIN=;;...; 注入; + * meow_ui_nav_init() 读之初始化栈深/顶层。App 自行 push/pop 页面后经 meow_ui_nav_report() + * 上报新深度/顶层。 + */ +#ifndef MEOW_UI_NAV_H +#define MEOW_UI_NAV_H + +#include + +#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 */ diff --git a/sdk/meow_ui_notify.c b/sdk/meow_ui_notify.c new file mode 100644 index 0000000..489ab52 --- /dev/null +++ b/sdk/meow_ui_notify.c @@ -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 +#include +#include +#include + +/* ---- 内部活动表尺寸(配额见 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]; +} diff --git a/sdk/meow_ui_sys.c b/sdk/meow_ui_sys.c new file mode 100644 index 0000000..6bce425 --- /dev/null +++ b/sdk/meow_ui_sys.c @@ -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 +#include +#include +#include +#include +#include + +#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; +} diff --git a/sdk/meow_ui_sys.h b/sdk/meow_ui_sys.h new file mode 100644 index 0000000..d8c0866 --- /dev/null +++ b/sdk/meow_ui_sys.h @@ -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 */ diff --git a/sdk/meow_ui_theme.c b/sdk/meow_ui_theme.c new file mode 100644 index 0000000..8ad3a35 --- /dev/null +++ b/sdk/meow_ui_theme.c @@ -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 +#include +#include +#include +#include + +#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(由调用方决定回退策略)。 + * - startend:跨午夜(如 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; + } + } +} diff --git a/sdk/meow_ui_view.c b/sdk/meow_ui_view.c new file mode 100644 index 0000000..e1dc8e1 --- /dev/null +++ b/sdk/meow_ui_view.c @@ -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 +#include + +#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; } diff --git a/sdk/meow_ui_view.h b/sdk/meow_ui_view.h new file mode 100644 index 0000000..729d703 --- /dev/null +++ b/sdk/meow_ui_view.h @@ -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 */ diff --git a/sdk/meow_ui_vol.c b/sdk/meow_ui_vol.c new file mode 100644 index 0000000..b10cf35 --- /dev/null +++ b/sdk/meow_ui_vol.c @@ -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 +#include +#include + +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 键:流式拷贝原文件到 .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); +} diff --git a/sdk/meow_ui_vol.h b/sdk/meow_ui_vol.h new file mode 100644 index 0000000..d0c71c1 --- /dev/null +++ b/sdk/meow_ui_vol.h @@ -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 */ diff --git a/sdk/meow_ui_widget.c b/sdk/meow_ui_widget.c new file mode 100644 index 0000000..4f8ef37 --- /dev/null +++ b/sdk/meow_ui_widget.c @@ -0,0 +1,1624 @@ +/* + * meow_ui_widget.c — 控件层(B1.3):按钮/滑条/开关/列表最小集。 + * + * 控件只负责绘制与状态;命中分发由应用在 on_tap 里调 meow_widget_hit 完成 + * (与 MEOW_APP_SDK.md §5 一致)。绘制读当前主题(meow_theme_get)走 back_buf。 + */ +#include "meow_ui.h" +#include "meow_ui_internal.h" +#include +#include +#include + +enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3, + W_CUSTOM = 4, W_LAYOUT = 5, W_SPACER = 6, + W_TEXTFIELD = 7, W_CARD = 8, W_DIVIDER = 9, W_BADGE = 10, + W_CHECKBOX = 11, W_RADIO = 12, W_PROGRESS = 13, + W_TOPAPPBAR = 14, W_NAVBAR = 15, W_TABBAR = 16, W_FAB = 17, + W_LISTVIEW = 18 }; /* §5.5 结构化列表(行=图标+标题+副标题+右侧操作+选中) */ + +/* 布局子项数组上限(固定尺寸子项先做;超出静默拒收)。 */ +#define MEOW_LAYOUT_MAX_CHILD 32 + +/* 容器(TopAppBar 动作 / NavigationBar 项 / TabBar 页签)子项上限。 */ +#define MEOW_CONTAINER_MAX 8 + +/* 按钮「按住」判定阈值:视觉上区分「短按按下」与「按住」(更深状态层)。 + * 阈值取 meow_ui_long_press_ms()(§6.3 通用长按默认 500ms,App 可配)——与长按事件同源, + * 避免「长按已触发但视觉未加深」的不一致。触发语义不变(on_tap 仍按抬手判定)。 */ + +/* 圆形 ProgressBar 环带厚 / 外扩边距(bgg 修复 M2.71-bug1:控件几何必须覆盖真实像素)。 + * 实际像素范围 = 中心 ± (r + thk/2);margin = thk/2 + 1 保证外接框覆盖环带外缘与圆帽。 */ +#define MEOW_PROGRESS_ARC_THK 4 +#define MEOW_PROGRESS_ARC_MARGIN (MEOW_PROGRESS_ARC_THK / 2 + 1) + +/* MD3 状态层混色:c 为基色、o 为状态色、t 为 o 的占比(0-100)。 + * 用于把主题 accent 向黑/白调出「按下/按住」状态层(日/夜主题各配色)。 */ +static uint32_t blend_color(uint32_t c, uint32_t o, int t) +{ + int rr = (((c >> 16) & 0xFF) * (100 - t) + ((o >> 16) & 0xFF) * t) / 100; + int gg = (((c >> 8) & 0xFF) * (100 - t) + ((o >> 8) & 0xFF) * t) / 100; + int bb = ((c & 0xFF) * (100 - t) + (o & 0xFF) * t) / 100; + return ((uint32_t)rr << 16) | ((uint32_t)gg << 8) | (uint32_t)bb; +} + +struct meow_widget { + int type; + int x, y, w, h; + int owner; /* §6.1 ①:创建时绑定的 Activity;命中只在 owner==active 才生效 */ + char label[128]; + int pressed; /* 按钮按下视觉态(遗留字面;状态机用 MEOW_WS_PRESSED 位) */ + int min, max, val; /* 滑条 */ + int on; /* 开关 */ + int count, sel; /* 列表 */ + meow_list_source src; + void *ctx; + + /* §5.8 batch1:统一状态机(§5.8.3)+ 通用 Options(§5.8.2)+ 自定义回调(§5.8.1)+ 布局(§5.8.5) */ + unsigned state; /* MEOW_WS_* 位掩码(默认 MEOW_WS_ENABLED) */ + meow_widget_opts opts; /* 通用修饰(浅拷;opts_used==0 时按控件内建默认) */ + int opts_used; /* 1=set_opts 显式挂载过 */ + meow_widget_render_fn render_cb; /* W_CUSTOM:自绘回调 */ + meow_widget_hit_fn hit_cb; /* W_CUSTOM:自定义命中回调 */ + int kind; /* W_LAYOUT:MEOW_LAYOUT_ROW/COLUMN/BOX */ + int spacing; /* W_LAYOUT:项间间距 */ + meow_widget *children[MEOW_LAYOUT_MAX_CHILD]; /* W_LAYOUT:子控件 */ + int nchildren; + + /* §5.8.6 batch2 Material 子集(尾缀追加;calloc 归零,安全) */ + int variant; /* W_BTN:MEOW_BTN_* 变体; W_PROGRESS:0=线性/1=圆形 */ + char placeholder[64]; /* W_TEXTFIELD 提示文案 */ + char *buf; int buflen; /* W_TEXTFIELD 文本缓冲(App 持有)+ 当前长 */ + int maxlen; /* W_TEXTFIELD 上限(含 NUL) */ + meow_ime_mode ime_mode; /* W_TEXTFIELD IME 模式 */ + int value; /* W_PROGRESS:0-100 / -1=不定; W_BADGE:计数 */ + int item_h; /* W_LIST:行高/列宽(lazy),默认 44 */ + int scroll; /* W_LIST:滚动偏移(lazy) */ + int horizontal; /* W_LIST:1=横向(LazyRow) */ + int back_on; /* W_TOPAPPBAR:显示返回箭头 */ + char items[MEOW_CONTAINER_MAX][24]; /* W_TOPAPPBAR 动作 / W_NAVBAR 项 / W_TABBAR 页签 */ + int nitems; /* 上述项数 */ + int selected; /* W_NAVBAR / W_TABBAR 选中下标 */ + /* M2.71-bug8:W_LIST 行点击回调(可选)。 */ + meow_list_tap_fn tap_cb; /* 命中行回调(可 NULL) */ + void *tap_ctx; /* 回调 ctx */ + + /* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中(calloc 归零,安全) */ + meow_list2_source src2; /* 行数据源(meow_list_item) */ + int sel_mode; /* MEOW_LIST_SEL_* */ + uint32_t *sel_bits; /* 多选位图(sel_bits_n 个字;SINGLE 不分配) */ + int sel_bits_n; /* 位图字数 */ + meow_listview_tap_fn lv_tap_cb; /* 行点击回调(idx, op) */ + void *lv_tap_ctx; /* 回调 ctx */ +}; + +/* ---- §5.8 batch1 内部辅助:opts 取值(opts_used==0 时按控件内建默认)---- + * 注意:必须先定义 struct meow_widget,才能访问其成员(C 要求类型完整)。 */ +static uint32_t widget_bg(const meow_widget *w, const meow_theme *th, uint32_t def) +{ (void)th; return (w->opts_used && w->opts.bg) ? w->opts.bg : def; } +static uint32_t widget_fg(const meow_widget *w, const meow_theme *th, uint32_t def) +{ (void)th; return (w->opts_used && w->opts.fg) ? w->opts.fg : def; } +static int widget_radius(const meow_widget *w, int def) +{ return (w->opts_used && w->opts.radius >= 0) ? w->opts.radius : def; } +static int widget_pady(const meow_widget *w) +{ return (w->opts_used && w->opts.pad_y > 0) ? w->opts.pad_y : 0; } + +/* 是否禁用(状态机:MEOW_WS_ENABLED 未置位)。 */ +static int widget_disabled(const meow_widget *w) { return !(w->state & MEOW_WS_ENABLED); } + +/* MD3 状态层叠色:把基色向 onSurface 叠 t% 调出按下/按住/聚焦状态层(§5.8.4)。 */ +static uint32_t state_tint(uint32_t base, const meow_theme *th, int t) +{ return blend_color(base, th->onSurface, t); } + +/* ---- §5.8.2/§5.8.3 公共 API:Options + 状态机 ---- */ +int meow_widget_set_opts(meow_widget *w, const meow_widget_opts *opts) +{ + if (!w || !opts) return -1; + w->opts = *opts; + w->opts_used = 1; + /* disabled/selected 立即反映到状态机位(§5.8.3)。 */ + if (opts->disabled) w->state &= ~MEOW_WS_ENABLED; + else w->state |= MEOW_WS_ENABLED; + if (opts->selected) w->state |= MEOW_WS_SELECTED; + else w->state &= ~MEOW_WS_SELECTED; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 挂载会改变绘制:标区域脏 */ + return 0; +} + +int meow_widget_opts_get(const meow_widget *w, meow_widget_opts *out) +{ + if (!w || !out) return -1; + if (!w->opts_used) { + /* 默认视图:radius=-1(控件默认)、disabled/selected 取状态位。 */ + memset(out, 0, sizeof *out); + out->radius = -1; + out->disabled = widget_disabled(w) ? 1 : 0; + out->selected = (w->state & MEOW_WS_SELECTED) ? 1 : 0; + return 0; + } + *out = w->opts; + return 0; +} + +void meow_widget_set_state(meow_widget *w, unsigned bits, int on) +{ + if (!w) return; + if (on) w->state |= bits; + else w->state &= ~bits; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 状态变化会改变绘制 */ +} +unsigned meow_widget_state(const meow_widget *w) { return w ? w->state : MEOW_WS_ENABLED; } +int meow_widget_enabled(const meow_widget *w) { return w ? (w->state & MEOW_WS_ENABLED) != 0 : 0; } +void meow_widget_set_enabled(meow_widget *w, int on) { meow_widget_set_state(w, MEOW_WS_ENABLED, on ? 1 : 0); } +int meow_widget_selected(const meow_widget *w) { return w ? (w->state & MEOW_WS_SELECTED) != 0 : 0; } +void meow_widget_set_selected(meow_widget *w, int on) { meow_widget_set_state(w, MEOW_WS_SELECTED, on ? 1 : 0); } + +/* 一键叠状态层(自定义画布可选;标准控件由 SDK 自动叠,不调用):半透明状态层 + 焦点描边。 */ +void meow_widget_draw_state_layer(meow_widget *w, const meow_theme *th) +{ + if (!w || !th) return; + if (widget_disabled(w)) { + meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + return; /* 禁用不再叠按下/聚焦 */ + } + if (w->state & MEOW_WS_PRESSED) { + /* 向 onSurface 叠半透明状态层铺满控件矩形(按下/按住) */ + int t = (w->opts_used && w->opts.bg) ? 24 : 12; + meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, t * 255 / 100); + } + if (w->state & MEOW_WS_FOCUSED) { + int r = widget_radius(w, 10); + meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); + } +} + +/* ---- 实时按压即时刷新(agent-119)---- + * 按压/按住视觉由 meow_widget_draw 读实时触摸(meow_input_touch_*)呈现,但 app 只在 + * on_tick(1s)/on_tap 重绘 → 快速点/按住时按压态刷新滞后/漏。此处在 SDK 触摸按下/释放/ + * 按住达阈值时主动重绘命中的按钮区域并局部上屏(不等 app 重绘),即时反映按下变深/ + * 释放恢复,连续快速点击每按都反映。仅针对 SDK 控件按钮(W_BTN);app 纯自绘不适用。 + * 语义不变:on_tap / meow_btn_pressed / meow_widget_hit 零改动;非按钮区域不动。 */ +#define MEOW_LIVE_BTN_MAX 64 +static meow_widget *g_live_btns[MEOW_LIVE_BTN_MAX]; +static int g_live_btn_n = 0; +static meow_widget *g_live_btn = NULL; /* 实时按压中最上层按钮(无 = NULL) */ +static int g_live_prev_hold = 0; /* 该按钮上次渲染是否为按住态 */ + +static void live_btn_register(meow_widget *w) +{ + if (!w || w->type != W_BTN) return; + for (int i = 0; i < g_live_btn_n; i++) + if (g_live_btns[i] == w) return; + if (g_live_btn_n < MEOW_LIVE_BTN_MAX) g_live_btns[g_live_btn_n++] = w; +} + +static void live_btn_unregister(meow_widget *w) +{ + for (int i = 0; i < g_live_btn_n; i++) { + if (g_live_btns[i] == w) { + g_live_btns[i] = g_live_btns[--g_live_btn_n]; + if (g_live_btn == w) g_live_btn = NULL; + return; + } + } + if (g_live_btn == w) g_live_btn = NULL; +} + +/* 命中:返回坐标所在的「当前 active Activity」的最上层按钮(创建序 = 绘制序,最后者在上)。 + * §6.1 ①:仅扫描 owner == 当前 active Activity 的按钮 —— 非 active Activity 的控件 + * 绝不参与命中(根治 agent-119 遗留的「全屏页触摸到主页按钮位置 → 闪现 accent 底」)。 */ +static meow_widget *live_btn_hit(int x, int y) +{ + int active = meow_ui_activity_active(); + meow_widget *r = NULL; + for (int i = 0; i < g_live_btn_n; i++) { + meow_widget *w = g_live_btns[i]; + if (w->owner != active) continue; /* 非 active activity:跳过 */ + if (x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h) + r = w; /* 越后创建越上:保留最后命中 */ + } + return r; +} + +/* 重绘并局部刷新一个按钮区。 + * §6.2 收敛:若按钮已注册绘制物 → 走 meow_ui_invalidate(清底 + 按 z 重画重叠区完整 z 栈), + * 避免按钮压上层 chrome 时被单控件 flush 抹掉(P1);未注册则回退单控件绘制+区域上屏。 */ +static void live_btn_redraw(meow_widget *w) +{ + if (!w) return; + if (meow_draw_widget_registered(w)) { + meow_ui_invalidate(w->x, w->y, w->w, w->h); + return; + } + meow_widget_draw(w); + meow_ui_flush_region(w->x, w->y, w->w, w->h); +} + +/* 由 meow_ui_run 每帧调用(主线程):检测实时触摸状态迁移,即时重绘命中按钮的按压/按住/ + * 释放,不等 app on_tick/on_tap。仅在触摸状态相对上次渲染发生变化时动作(非全屏、省性能)。 */ +void meow_widget_touch_refresh(void) +{ + int active = meow_ui_activity_active(); + /* §6.1 ①:若当前按住按钮已不属于 active activity(按压期间发生了 Activity 切换, + * 例如点 FULLSCREEN 后进入全屏),静默清引用——不绘制、不闪现(否则会在新页面底上 + * 画出旧按钮的 accent 残留)。 */ + if (g_live_btn && g_live_btn->owner != active) { + g_live_btn->state &= ~MEOW_WS_PRESSED; /* §5.8.3:清除按压态(跨页面不残留) */ + g_live_btn = NULL; + g_live_prev_hold = 0; + } + int down = meow_input_touch_down(); + meow_widget *hit = down ? live_btn_hit(meow_input_touch_x(), meow_input_touch_y()) : NULL; + int hold = down && hit && meow_input_touch_hold_ms() >= (unsigned)meow_ui_long_press_ms(); + + if (!down) { + /* 释放:把最近按压按钮恢复为常态 */ + if (g_live_btn) { + g_live_btn->state &= ~MEOW_WS_PRESSED; + live_btn_redraw(g_live_btn); + g_live_btn = NULL; + } + g_live_prev_hold = 0; + return; + } + if (hit != g_live_btn) { + /* 按下/滑至新按钮:先恢复旧(若手指滑离),再按压新 */ + if (g_live_btn) { + g_live_btn->state &= ~MEOW_WS_PRESSED; + live_btn_redraw(g_live_btn); + } + g_live_btn = hit; + g_live_prev_hold = hold; + if (hit) { + if (widget_disabled(hit)) { g_live_btn = NULL; } /* 禁用不响应按压 */ + else { hit->state |= MEOW_WS_PRESSED; live_btn_redraw(hit); } + } + } else if (hit && hold != g_live_prev_hold) { + /* 同一按钮按住达阈值:升级到更强态 */ + hit->state |= MEOW_WS_PRESSED; + live_btn_redraw(hit); + g_live_prev_hold = hold; + } +} + +meow_widget *meow_btn_create(int x, int y, int w, int h, const char *label) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_BTN; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); /* §6.1 ①:绑定当前 active Activity */ + t->state = MEOW_WS_ENABLED; /* §5.8.3:默认 ENABLED */ + if (label) { strncpy(t->label, label, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } + live_btn_register(t); + meow_draw_widget_register(t); /* §6.2 自动注册(创建即注册,App 零调用) */ + return t; +} + +meow_widget *meow_slider_create(int x, int y, int w, int min, int max, int val) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_SLIDER; t->x = x; t->y = y; t->w = w; t->h = 40; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->min = min; t->max = (max > min) ? max : min + 1; t->val = (val < min) ? min : (val > t->max) ? t->max : val; + meow_draw_widget_register(t); + return t; +} + +meow_widget *meow_switch_create(int x, int y, int on) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_SWITCH; t->x = x; t->y = y; t->w = 56; t->h = 32; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->on = on ? 1 : 0; + if (t->on) t->state |= MEOW_WS_SELECTED; /* §5.8.3:开关 on → selected */ + meow_draw_widget_register(t); + return t; +} + +meow_widget *meow_list_create(int x, int y, int w, int h, int count, + meow_list_source src, void *ctx) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_LIST; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->count = count; t->src = src; t->ctx = ctx; t->sel = 0; + meow_draw_widget_register(t); + return t; +} + +void meow_widget_free(meow_widget *w) +{ + if (!w) return; + meow_draw_widget_unregister(w); /* §6.2:绘制物注册表自动注销(stale id 失效勿用) */ + live_btn_unregister(w); + if (w->sel_bits) { free(w->sel_bits); w->sel_bits = NULL; } /* W_LISTVIEW 多选位图 */ + free(w); +} + +/* ---- §5.8.1 自定义画布:App 提供 render/hit 回调,同一个 meow_widget 基座 ---- */ +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) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_CUSTOM; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); /* §6.1 ①:同既有控件绑当前 active Activity */ + t->state = MEOW_WS_ENABLED; + t->render_cb = render_cb; + t->hit_cb = hit_cb; + t->ctx = ctx; + meow_draw_widget_register(t); + return t; +} + +/* ---- §5.8.5 可组合布局:Row / Column / Box + Spacer ---- */ +meow_widget *meow_row_create(int x, int y, int w, int h, int spacing) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_LAYOUT; t->kind = MEOW_LAYOUT_ROW; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->spacing = spacing > 0 ? spacing : 0; + meow_draw_widget_register(t); + return t; +} +meow_widget *meow_column_create(int x, int y, int w, int h, int spacing) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_LAYOUT; t->kind = MEOW_LAYOUT_COLUMN; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->spacing = spacing > 0 ? spacing : 0; + meow_draw_widget_register(t); + return t; +} +meow_widget *meow_box_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_LAYOUT; t->kind = MEOW_LAYOUT_BOX; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->spacing = 0; + meow_draw_widget_register(t); + return t; +} +meow_widget *meow_layout_spacer(int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_SPACER; t->x = 0; t->y = 0; t->w = w > 0 ? w : 0; t->h = h > 0 ? h : 0; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + meow_draw_widget_register(t); + return t; +} + +int meow_layout_add(meow_widget *layout, meow_widget *child) +{ + if (!layout || layout->type != W_LAYOUT || !child) return -1; + if (layout->nchildren >= MEOW_LAYOUT_MAX_CHILD) return -1; + for (int i = 0; i < layout->nchildren; i++) + if (layout->children[i] == child) return -1; /* 同子项去重 */ + layout->children[layout->nchildren++] = child; + return 0; +} + +/* 把子控件算成绝对坐标(写进各 child 的 x,y;w/h 固定尺寸,先不做弹性)。 + * Row:子项水平排(垂直居中于容器 h);Column:垂直排(水平居中);Box:叠放(各放容器左上角)。 + * 子项的 margin_x/margin_y(Options)参与排布;子项为布局时递归 arrange。 */ +void meow_layout_arrange(meow_widget *layout) +{ + if (!layout || layout->type != W_LAYOUT) return; + int kind = layout->kind; + int cur = (kind == MEOW_LAYOUT_COLUMN) ? layout->y : layout->x; + for (int i = 0; i < layout->nchildren; i++) { + meow_widget *c = layout->children[i]; + if (!c) continue; + int mx = (c->opts_used && c->opts.margin_x > 0) ? c->opts.margin_x : 0; + int my = (c->opts_used && c->opts.margin_y > 0) ? c->opts.margin_y : 0; + if (kind == MEOW_LAYOUT_ROW) { + cur += mx; + c->x = cur; + c->y = layout->y + (layout->h - c->h) / 2; /* 垂直居中 */ + if (c->y < layout->y) c->y = layout->y; + cur += c->w + mx + layout->spacing; + } else if (kind == MEOW_LAYOUT_COLUMN) { + cur += my; + c->y = cur; + c->x = layout->x + (layout->w - c->w) / 2; /* 水平居中 */ + if (c->x < layout->x) c->x = layout->x; + cur += c->h + my + layout->spacing; + } else { /* BOX:叠放,各放容器左上角 */ + c->x = layout->x; + c->y = layout->y; + } + if (c->type == W_LAYOUT) meow_layout_arrange(c); /* 嵌套递归 */ + } +} + +/* ---- 布局深度反推访问器(meow_ui_draw.c 用,读本 TU 的 struct meow_widget 内部字段)---- */ +int meow_widget_is_layout(const meow_widget *w) +{ return w && w->type == W_LAYOUT; } +int meow_widget_nchild(const meow_widget *w) +{ return (w && w->type == W_LAYOUT) ? w->nchildren : 0; } +const meow_widget *meow_widget_child_at(const meow_widget *w, int idx) +{ return (w && w->type == W_LAYOUT && idx >= 0 && idx < w->nchildren) ? w->children[idx] : NULL; } + +/* ---- 几何/归属访问器(meow_ui_draw.c 用:绘制物 rect 实时读 + owner 过滤)---- */ +int meow_widget_owner(const meow_widget *w) +{ return w ? w->owner : 0; } +void meow_widget_rect(const meow_widget *w, int out[4]) +{ + if (!out) return; + if (!w) { out[0] = out[1] = out[2] = out[3] = 0; return; } + out[0] = w->x; out[1] = w->y; out[2] = w->w; out[3] = w->h; +} + +/* ============================================================ */ +/* §5.8.6 batch2 Material 组件子集 API(create/setter/getter/hit) */ +/* ============================================================ */ + +/* ---- Button 五型(既有 meow_btn_create = FILLED) ---- */ +meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *label, int variant) +{ + meow_widget *t = meow_btn_create(x, y, w, h, label); + if (!t) return NULL; + if (variant < MEOW_BTN_FILLED || variant > MEOW_BTN_ELEVATED) variant = MEOW_BTN_FILLED; + t->variant = variant; + return t; +} +int meow_btn_set_variant(meow_widget *w, int variant) +{ + if (!w || w->type != W_BTN) return -1; + if (variant < MEOW_BTN_FILLED || variant > MEOW_BTN_ELEVATED) return -1; + if (variant == w->variant) return 0; + w->variant = variant; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return 0; +} + +/* ---- TextField ---- */ +meow_widget *meow_textfield_create(int x, int y, int w, int h, const char *placeholder, + char *buf, int maxlen, meow_ime_mode mode) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_TEXTFIELD; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + if (placeholder) { + strncpy(t->placeholder, placeholder, sizeof t->placeholder - 1); + t->placeholder[sizeof t->placeholder - 1] = 0; + } + t->buf = buf; t->buflen = buf ? (int)strlen(buf) : 0; + t->maxlen = (maxlen > 0 && maxlen <= 256) ? maxlen : 256; + t->ime_mode = mode; + meow_draw_widget_register(t); + return t; +} +void meow_textfield_set_placeholder(meow_widget *w, const char *placeholder) +{ + if (!w || w->type != W_TEXTFIELD) return; + if (placeholder) { + strncpy(w->placeholder, placeholder, sizeof w->placeholder - 1); + w->placeholder[sizeof w->placeholder - 1] = 0; + } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +const char *meow_textfield_text(meow_widget *w) { return (w && w->type == W_TEXTFIELD && w->buf) ? w->buf : ""; } +/* 输入中实时把 IME 文本同步到字段缓冲 + 重画字段区(invalidate 重画 z 栈相交绘制物; + * flush 在 IME 阻塞循环内被挂起(meow_gfx_flush_hold),随本周期末一次原子上屏) */ +static void textfield_change_cb(const char *text, void *ctx) +{ + meow_widget *w = (meow_widget *)ctx; + if (!w || w->type != W_TEXTFIELD) return; + if (w->buf && text) { + strncpy(w->buf, text, w->maxlen - 1); + w->buf[w->maxlen - 1] = 0; + } + w->buflen = text ? (int)strlen(text) : 0; + /* 只标脏不重画的话 back_buf 仍是旧文本,flush 上去的还是旧的——必须真重画 */ + meow_ui_invalidate(w->x, w->y, w->w, w->h); +} +int meow_textfield_activate(meow_widget *w) +{ + if (!w || w->type != W_TEXTFIELD) return 0; + w->state |= MEOW_WS_FOCUSED; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + meow_ime_ctx ctx; + memset(&ctx, 0, sizeof ctx); + ctx.mode = w->ime_mode; + ctx.maxlen = w->maxlen; + ctx.on_change = textfield_change_cb; + ctx.ctx = w; + if (w->buf) strncpy(ctx.buf, w->buf, sizeof ctx.buf - 1); /* 预填当前文本 */ + int r = meow_ui_text_input(&ctx); + if (r && w->buf) { + strncpy(w->buf, ctx.buf, w->maxlen - 1); + w->buf[w->maxlen - 1] = 0; + } + w->buflen = w->buf ? (int)strlen(w->buf) : 0; + w->state &= ~MEOW_WS_FOCUSED; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return r; +} + +/* ---- Card ---- */ +meow_widget *meow_card_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_CARD; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + meow_draw_widget_register(t); + return t; +} +void meow_card_set_content(meow_widget *card, meow_widget *child) +{ + if (!card || card->type != W_CARD || !child) return; + card->children[0] = child; + card->nchildren = 1; +} + +/* ---- Divider ---- */ +meow_widget *meow_divider_create(int x, int y, int len, int vertical) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_DIVIDER; t->x = x; t->y = y; + t->w = vertical ? 1 : (len > 0 ? len : 1); + t->h = vertical ? (len > 0 ? len : 1) : 1; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->variant = vertical ? 1 : 0; + meow_draw_widget_register(t); + return t; +} + +/* ---- Badge ---- */ +meow_widget *meow_badge_create(int cx, int cy) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_BADGE; t->x = cx; t->y = cy; t->w = 24; t->h = 24; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->value = 0; /* 红点 */ + meow_draw_widget_register(t); + return t; +} +void meow_badge_set(meow_widget *w, int count) +{ + if (!w || w->type != W_BADGE) return; + w->value = count; + meow_gfx_mark_dirty(w->x - 16, w->y - 16, 32, 32); +} + +/* ---- Checkbox / Radio ---- */ +meow_widget *meow_checkbox_create(int x, int y) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_CHECKBOX; t->x = x; t->y = y; t->w = 34; t->h = 34; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + meow_draw_widget_register(t); + return t; +} +meow_widget *meow_radio_create(int x, int y) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_RADIO; t->x = x; t->y = y; t->w = 34; t->h = 34; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + meow_draw_widget_register(t); + return t; +} + +/* ---- ProgressBar ---- */ +meow_widget *meow_progress_create(int x, int y, int w, int h, int value) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_PROGRESS; t->x = x; t->y = y; t->w = w; t->h = h > 0 ? h : 4; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->variant = 0; /* 线性 */ + t->value = value < 0 ? -1 : (value > 100 ? 100 : value); + meow_draw_widget_register(t); + return t; +} +meow_widget *meow_progress_circular_create(int cx, int cy, int r, int value) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + /* bbox = 整圆像素外接框(中心 ± (r + margin)):使 w->x/y/w/h 即真实绘制范围, + * 于是 meow_progress_set 的 mark_dirty 与 App 的 flush_region(w->x,w->y,w->w,w->h) + * 都天然对齐,不漏刷/不多刷(修复 bgg M2.71 圆形进度脏区)。 */ + int m = MEOW_PROGRESS_ARC_MARGIN; + t->type = W_PROGRESS; + t->x = cx - r - m; t->y = cy - r - m; + t->w = 2 * (r + m); t->h = 2 * (r + m); + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->variant = 1; /* 圆形 */ + t->value = value < 0 ? -1 : (value > 100 ? 100 : value); + meow_draw_widget_register(t); + return t; +} +void meow_progress_set(meow_widget *w, int value) +{ + if (!w || w->type != W_PROGRESS) return; + w->value = value < 0 ? -1 : (value > 100 ? 100 : value); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} + +/* ---- FAB ---- */ +meow_widget *meow_fab_create(int x, int y, int size, const char *label) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_FAB; t->x = x; t->y = y; t->w = size > 0 ? size : 48; t->h = t->w; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + if (label) { strncpy(t->label, label, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } + meow_draw_widget_register(t); + return t; +} + +/* ---- TopAppBar ---- */ +meow_widget *meow_topappbar_create(int x, int y, int w, int h, const char *title) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_TOPAPPBAR; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->back_on = 0; t->nitems = 0; + if (title) { strncpy(t->label, title, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; } + meow_draw_widget_register(t); + return t; +} +void meow_topappbar_set_title(meow_widget *w, const char *title) +{ + if (!w || w->type != W_TOPAPPBAR) return; + if (title) { strncpy(w->label, title, sizeof w->label - 1); w->label[sizeof w->label - 1] = 0; } + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +void meow_topappbar_set_back(meow_widget *w, int on) +{ + if (!w || w->type != W_TOPAPPBAR) return; + w->back_on = on ? 1 : 0; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_topappbar_add_action(meow_widget *w, const char *label) +{ + if (!w || w->type != W_TOPAPPBAR || w->nitems >= MEOW_CONTAINER_MAX) return -1; + if (label) strncpy(w->items[w->nitems], label, 23); + w->items[w->nitems][23] = 0; + int idx = w->nitems; + w->nitems++; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return idx; +} +int meow_topappbar_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_TOPAPPBAR) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + if (w->back_on && x >= w->x && x < w->x + 44) return MEOW_TOPBAR_BACK; + const meow_theme *th = meow_theme_get(); + int ax = w->x + w->w - 10; + for (int i = w->nitems - 1; i >= 0; i--) { + int aw = meow_ui_text_width(w->items[i], th->font_body) + 12; + ax -= aw; + if (x >= ax && x < ax + aw && x >= w->x + 100) return i; /* 不压标题区/返回区 */ + } + return -1; +} + +/* ---- NavigationBar ---- */ +meow_widget *meow_navigationbar_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_NAVBAR; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->nitems = 0; t->selected = 0; + meow_draw_widget_register(t); + return t; +} +int meow_navigationbar_add_item(meow_widget *w, const char *label) +{ + if (!w || w->type != W_NAVBAR || w->nitems >= MEOW_CONTAINER_MAX) return -1; + if (label) strncpy(w->items[w->nitems], label, 23); + w->items[w->nitems][23] = 0; + int idx = w->nitems; + w->nitems++; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return idx; +} +void meow_navigationbar_set_selected(meow_widget *w, int idx) +{ + if (!w || w->type != W_NAVBAR) return; + if (idx < 0) idx = 0; + if (idx >= w->nitems) idx = w->nitems - 1; + w->selected = idx; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_navigationbar_selected(meow_widget *w) { return (w && w->type == W_NAVBAR) ? w->selected : 0; } +int meow_navigationbar_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_NAVBAR || w->nitems <= 0) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int iw = w->w / w->nitems; + int idx = iw > 0 ? (x - w->x) / iw : 0; + return (idx >= 0 && idx < w->nitems) ? idx : -1; +} + +/* ---- TabBar ---- */ +meow_widget *meow_tabbar_create(int x, int y, int w, int h) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_TABBAR; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->nitems = 0; t->selected = 0; + meow_draw_widget_register(t); + return t; +} +int meow_tabbar_add_tab(meow_widget *w, const char *label) +{ + if (!w || w->type != W_TABBAR || w->nitems >= MEOW_CONTAINER_MAX) return -1; + if (label) strncpy(w->items[w->nitems], label, 23); + w->items[w->nitems][23] = 0; + int idx = w->nitems; + w->nitems++; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return idx; +} +void meow_tabbar_set_selected(meow_widget *w, int idx) +{ + if (!w || w->type != W_TABBAR) return; + if (idx < 0) idx = 0; + if (idx >= w->nitems) idx = w->nitems - 1; + w->selected = idx; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_tabbar_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_TABBAR || w->nitems <= 0) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int iw = w->w / w->nitems; + int idx = iw > 0 ? (x - w->x) / iw : 0; + return (idx >= 0 && idx < w->nitems) ? idx : -1; +} + +/* ---- 列表容器(LazyColumn/LazyRow:虚拟化 + 滚动;复用 W_LIST 与 meow_list_source)---- */ +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 *t = meow_list_create(x, y, w, h, count, src, ctx); + if (!t) return NULL; + t->item_h = item_h > 0 ? item_h : 44; + t->scroll = 0; t->horizontal = 0; + return t; +} +meow_widget *meow_lazyrow_create(int x, int y, int w, int h, int item_w, int count, + meow_list_source src, void *ctx) +{ + meow_widget *t = meow_list_create(x, y, w, h, count, src, ctx); + if (!t) return NULL; + t->item_h = item_w > 0 ? item_w : 60; + t->scroll = 0; t->horizontal = 1; + return t; +} +static int list_item(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 44; } +int meow_list_max_scroll(meow_widget *w) +{ + if (!w || w->type != W_LIST) return 0; + int vp = w->horizontal ? w->w : w->h; + int ms = w->count * list_item(w) - vp; + return ms < 0 ? 0 : ms; +} +void meow_list_set_scroll(meow_widget *w, int offset) +{ + if (!w || w->type != W_LIST) return; + int maxs = meow_list_max_scroll(w); + if (offset < 0) offset = 0; + if (offset > maxs) offset = maxs; + if (offset == w->scroll) return; + w->scroll = offset; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_list_scroll(meow_widget *w) { return (w && w->type == W_LIST) ? w->scroll : 0; } +void meow_list_scroll_by(meow_widget *w, int dy) { if (w) meow_list_set_scroll(w, w->scroll + dy); } +int meow_list_set_item_h(meow_widget *w, int item) +{ + if (!w || w->type != W_LIST) return -1; + if (item > 0) { w->item_h = item; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } + return w->item_h; +} +int meow_list_hit_index(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_LIST) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int item = list_item(w); + int idx = w->horizontal ? (x - w->x + w->scroll) / item + : (y - w->y + w->scroll) / item; + return (idx >= 0 && idx < w->count) ? idx : -1; +} + +/* M2.71-bug8:选中行(越界 clamp)+ 标脏(高亮刷新)。 */ +void meow_list_set_selected(meow_widget *w, int idx) +{ + if (!w || w->type != W_LIST) return; + if (idx < 0) idx = 0; + if (w->count > 0 && idx >= w->count) idx = w->count - 1; + if (idx == w->sel) return; + w->sel = idx; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +int meow_list_selected(meow_widget *w) { return (w && w->type == W_LIST) ? w->sel : -1; } + +void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx) +{ + if (!w || w->type != W_LIST) return; + w->tap_cb = cb; w->tap_ctx = ctx; +} + +/* 命中行:置选中 + 回调(若设)+ 返回 idx;未命中返回 -1(不置选中)。 */ +int meow_list_tap_at(meow_widget *w, int x, int y) +{ + int idx = meow_list_hit_index(w, x, y); + if (idx < 0) return -1; + meow_list_set_selected(w, idx); + if (w->tap_cb) w->tap_cb(idx, w->tap_ctx); + return idx; +} + +/* ============================================================ */ +/* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中 + * ------------------------------------------------------------ + * 行数据源(meow_list2_source)逐行填充 meow_list_item;容器只画可见行(虚拟化)+ 视口裁剪 + * (M2.73 纪律:只画视口内行/分隔线/文本,无标题栏/底部泄漏);局部 dirty;行高固定;首/末行 + * 钳制。颜色全部取 meow_theme_get() 语义色(日/夜/style 自动),禁止硬编码。 */ +/* ============================================================ */ +#define MEOW_LISTV_ICON 24 /* 图标盒尺寸(px) */ +#define MEOW_LISTV_PAD 16 /* 行内左右 padding(px) */ +#define MEOW_LISTV_SUB_GAP 2 /* 标题与副标题间隙(px) */ + +/* meow_ui_img_scaled 弱引用:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支用它绘制图片图标。 + * 弱引用让「只用内置符号图标、不解码图片」的 App 不被牵连进 meow_ui_img.o/stb_image.o + * (静态库惰性链接:无强引用则不拉入 stb 解码器,避免所有 App 无谓膨胀 ~77KB)。 + * 宿主/设备需图片图标的 App 经 meow_ui_img_load_* 强引用 meow_ui_img.o → 此处自动解析。 */ +extern void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *img) __attribute__((weak)); + +static int listv_item_h(const meow_widget *w) { return (w->item_h > 0) ? w->item_h : 56; } + +/* 文本截断:超出 maxw 加 "..."(复用 browser_text_trunc 的逐字符宽度累加)。 */ +static void listv_text_trunc(int x, int y, const char *s, int maxw, int size, uint32_t col) +{ + if (!s) return; + int full = meow_ui_text_width(s, size); + if (full <= maxw) { meow_ui_text(x, y, s, size, col); return; } + const char *dots = "..."; + int dw = meow_ui_text_width(dots, size); + char tmp[128]; + int i; + for (i = 0; s[i] && i < (int)sizeof tmp - 4; i++) { + tmp[i] = s[i]; + tmp[i + 1] = '\0'; + if (meow_ui_text_width(tmp, size) > maxw - dw) break; + } + memcpy(tmp + i, dots, 4); + meow_ui_text(x, y, tmp, size, col); +} + +/* 行图标绘制(内置符号 / meow_img)。col=图标前景(选中/未选中由调用方定)。 */ +static void listv_draw_icon(const meow_theme *th, int x, int y, int kind, + const meow_img *img, uint32_t col) +{ + (void)th; + int s = MEOW_LISTV_ICON; + if (kind == MEOW_LIST_ICON_IMG && img && meow_ui_img_scaled) { /* 弱符号非空才画(未链 stb 则跳过) */ + meow_ui_img_scaled(x, y, s, s, (meow_img *)img); /* img 为 opaque 句柄,const 丢弃 */ + return; + } + if (kind == MEOW_LIST_ICON_FOLDER) { + meow_ui_fill_rect(x, y + 6, s, s - 6, col); /* 主体 */ + meow_ui_fill_rect(x, y + 2, 12, 6, col); /* 页签 */ + } else if (kind == MEOW_LIST_ICON_FILE) { + meow_ui_draw_rect(x + 4, y + 2, s - 8, s - 4, col);/* 页体 */ + meow_ui_fill_rect(x + 4, y + 2, 7, 4, col); /* 文件名横条 */ + } +} + +/* 行右侧操作区绘制(箭头/开关/复选/⋯)。x=动作槽左缘;col=前景。 */ +static void listv_draw_right(const meow_theme *th, int x, int y, int row_h, + int right, int on, uint32_t col) +{ + switch (right) { + case MEOW_LIST_RIGHT_ARROW: { + int cx = x + 4, cy = y + row_h / 2; + meow_gfx_line(cx - 4, cy - 6, cx + 4, cy, 2, col); + meow_gfx_line(cx + 4, cy, cx - 4, cy + 6, 2, col); + break; + } + case MEOW_LIST_RIGHT_SWITCH: { + int sw = 36, sh = 20; + int sx = x, sy = y + (row_h - sh) / 2; + meow_gfx_fill_round(sx, sy, sw, sh, sh / 2, on ? th->primary : th->onSurfaceVariant); + int kr = sh / 2 - 3; + int kx = on ? sx + sw - kr * 2 - 3 : sx + 3; + meow_gfx_fill_round(kx, sy + 3, kr * 2, kr * 2, kr, on ? th->onPrimary : th->surface); + break; + } + case MEOW_LIST_RIGHT_CHECK: { + int cx = x + 2, cy = y + row_h / 2; + meow_gfx_line(cx - 6, cy + 2, cx - 1, cy + 7, 2, col); + meow_gfx_line(cx - 1, cy + 7, cx + 7, cy - 6, 2, col); + break; + } + case MEOW_LIST_RIGHT_MORE: { + int cx = x; + int cy0 = y + row_h / 2 - 8; + for (int d = 0; d < 3; d++) + meow_ui_fill_rect(cx, cy0 + d * 6, 4, 4, col); + break; + } + default: break; + } +} + +/* 多选位图扩容(MULTI 且 idx 超出当前容量时;SINGLE 不分配)。 */ +static void listv_bitmap_grow(meow_widget *w, int idx) +{ + int need = (idx + 1 + 31) / 32; + if (need <= w->sel_bits_n) return; + uint32_t *nb = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); + if (!nb) return; + if (w->sel_bits && w->sel_bits_n) + memcpy(nb, w->sel_bits, (size_t)w->sel_bits_n * sizeof(uint32_t)); + free(w->sel_bits); + w->sel_bits = nb; + w->sel_bits_n = need; +} + +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) +{ + meow_widget *t = (meow_widget *)calloc(1, sizeof *t); + if (!t) return NULL; + t->type = W_LISTVIEW; t->x = x; t->y = y; t->w = w; t->h = h; + t->owner = meow_ui_activity_active(); + t->state = MEOW_WS_ENABLED; + t->count = count > 0 ? count : 0; t->src2 = src; t->ctx = ctx; + t->sel = -1; t->item_h = 56; t->scroll = 0; + t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE; + if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) { + t->sel_bits_n = (t->count + 31) / 32; + t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t)); + } + meow_draw_widget_register(t); + return t; +} + +int meow_listview_set_item_h(meow_widget *w, int item) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (item > 0) { w->item_h = item; meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); } + return w->item_h; +} +void meow_listview_set_count(meow_widget *w, int count) +{ + if (!w || w->type != W_LISTVIEW) return; + if (count < 0) count = 0; + if (count == w->count) return; + w->count = count; + if (w->sel >= w->count) w->sel = -1; + if (w->sel_mode == MEOW_LIST_SEL_MULTI) { /* 重分配多选位图 */ + free(w->sel_bits); w->sel_bits = NULL; w->sel_bits_n = 0; + int need = (count + 31) / 32; + if (need > 0) { + w->sel_bits_n = need; + w->sel_bits = (uint32_t *)calloc((size_t)need, sizeof(uint32_t)); + } + } + int ms = meow_listview_max_scroll(w); /* 行数减少时 clamp scroll */ + if (w->scroll > ms) w->scroll = ms; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} + +int meow_listview_max_scroll(meow_widget *w) +{ + if (!w || w->type != W_LISTVIEW) return 0; + int ms = w->count * listv_item_h(w) - w->h; + return ms < 0 ? 0 : ms; +} + +void meow_listview_set_scroll(meow_widget *w, int offset) +{ + if (!w || w->type != W_LISTVIEW) return; + int ms = meow_listview_max_scroll(w); + if (offset < 0) offset = 0; + if (offset > ms) offset = ms; + if (offset == w->scroll) return; + w->scroll = offset; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); +} +void meow_listview_scroll_by(meow_widget *w, int dy) { if (w) meow_listview_set_scroll(w, w->scroll + dy); } +int meow_listview_scroll(meow_widget *w) { return (w && w->type == W_LISTVIEW) ? w->scroll : 0; } + +void meow_listview_set_sel(meow_widget *w, int idx) +{ + if (!w || w->type != W_LISTVIEW) return; + if (w->sel_mode == MEOW_LIST_SEL_NONE) return; + if (w->count <= 0) { w->sel = -1; return; } + if (idx >= w->count) idx = w->count - 1; /* 越界 clamp(§5.5 首/末行钳制) */ + if (w->sel_mode == MEOW_LIST_SEL_SINGLE) { + if (idx == w->sel) return; + w->sel = idx; /* idx<0 = 清除 */ + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } else { /* MULTI:翻转 idx */ + if (idx < 0) { + if (w->sel_bits) memset(w->sel_bits, 0, (size_t)w->sel_bits_n * sizeof(uint32_t)); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + return; + } + listv_bitmap_grow(w, idx); + if (!w->sel_bits) return; + w->sel_bits[idx >> 5] ^= (1u << (idx & 31)); + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } +} +int meow_listview_get_sel(meow_widget *w, int *out, int max) +{ + if (!w || w->type != W_LISTVIEW) return 0; + if (!out || max <= 0) return 0; + int n = 0; + if (w->sel_mode == MEOW_LIST_SEL_MULTI) { + for (int i = 0; i < w->count && n < max; i++) + if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) out[n++] = i; + } else if (w->sel >= 0 && n < max) { + out[n++] = w->sel; + } + return n; +} +int meow_listview_sel_count(meow_widget *w) +{ + if (!w || w->type != W_LISTVIEW) return 0; + if (w->sel_mode == MEOW_LIST_SEL_MULTI) { + int n = 0; + for (int i = 0; i < w->count; i++) + if (w->sel_bits && (w->sel_bits[i >> 5] & (1u << (i & 31)))) n++; + return n; + } + return (w->sel >= 0) ? 1 : 0; +} + +/* 命中:返回行号(含滚动换算;禁用行/越界 -1)。抬手用最新坐标(goodix 免疫,调用方传抬手落点)。 */ +int meow_listview_hit(meow_widget *w, int x, int y) +{ + if (!w || w->type != W_LISTVIEW) return -1; + if (x < w->x || x >= w->x + w->w || y < w->y || y >= w->y + w->h) return -1; + int item = listv_item_h(w); + int idx = (y - w->y + w->scroll) / item; + if (idx < 0 || idx >= w->count) return -1; + if (w->src2) { /* 禁用行不响应(§5.5) */ + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2(idx, &it, w->ctx)) return -1; + if (!it.enabled) return -1; + } + return idx; +} +int meow_listview_hit_op(meow_widget *w, int x, int y) +{ + int idx = meow_listview_hit(w, x, y); + if (idx < 0) return 0; + int opx = w->x + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5) */ + if (x < opx) return 0; + if (w->src2) { + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2(idx, &it, w->ctx)) return 0; + if (it.right == MEOW_LIST_RIGHT_NONE) return 0; + } + return 1; +} +void meow_listview_set_on_tap(meow_widget *w, meow_listview_tap_fn cb, void *ctx) +{ + if (!w || w->type != W_LISTVIEW) return; + w->lv_tap_cb = cb; w->lv_tap_ctx = ctx; +} +int meow_listview_tap_at(meow_widget *w, int x, int y) +{ + int idx = meow_listview_hit(w, x, y); + if (idx < 0) return -1; + int op = meow_listview_hit_op(w, x, y); + meow_listview_set_sel(w, idx); + if (w->lv_tap_cb) w->lv_tap_cb(idx, op, w->lv_tap_ctx); + return idx; +} + +static int hit(meow_widget *w, int x, int y) +{ + if (!w) return 0; + return x >= w->x && x < w->x + w->w && y >= w->y && y < w->y + w->h; +} + +int meow_widget_hit(meow_widget *w, int x, int y) +{ + if (!w) return 0; + /* §6.1 ①:页面控件只对自身 Activity 负责,不跨页面复用命中。 */ + if (w->owner != meow_ui_activity_active()) return 0; + /* §5.8.3:禁用不响应命中(也不进 pressed)。 */ + if (widget_disabled(w)) return 0; + /* §5.8.1:自定义画布用 App 回调判定命中(可非矩形);其余矩形内含。 */ + if (w->type == W_CUSTOM) + return w->hit_cb ? w->hit_cb(w, x, y, w->ctx) : hit(w, x, y); + return hit(w, x, y); +} +int meow_btn_pressed(meow_widget *w) +{ return w && w->type == W_BTN ? (w->pressed || (w->state & MEOW_WS_PRESSED)) : 0; } + +void meow_slider_set(meow_widget *w, int val) +{ + if (!w || w->type != W_SLIDER) return; + if (val < w->min) val = w->min; if (val > w->max) val = w->max; + if (val == w->val) return; + w->val = val; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); /* 滑块/轨道随值变化:标脏 */ +} +int meow_slider_get(meow_widget *w) { return w && w->type == W_SLIDER ? w->val : 0; } +void meow_switch_set(meow_widget *w, int on) +{ + if (w && w->type == W_SWITCH) { + w->on = on ? 1 : 0; + if (w->on) w->state |= MEOW_WS_SELECTED; /* §5.8.3:on → selected */ + else w->state &= ~MEOW_WS_SELECTED; + meow_gfx_mark_dirty(w->x, w->y, w->w, w->h); + } +} +int meow_switch_get(meow_widget *w) { return w && w->type == W_SWITCH ? w->on : 0; } + +/* 供应用在 on_tap 里对外可控回馈:这里不存命中态(由 app 分发),仅暴露绘制。 + * §5.8 升级:统一取语义色(meow_theme),读 opts(背景/圆角/描边/pad/fg)与状态机位 + * (disabled/selected/pressed/focused),标准控件由 SDK 自动叠状态层;自定义画布(W_CUSTOM) + * 由 App render_cb 全权自绘(SDK 只注入主题、不画背景/状态层);布局(W_LAYOUT)递归绘制子控件。 */ +void meow_widget_draw(meow_widget *w) +{ + if (!w) return; + /* 绘制期裁剪(§3):把绘制 clip 设为该控件矩形,画完恢复——任何控件(含 W_CUSTOM + * 自定义画布)不可能画出自身边界,从根上杜绝越界脏区。可嵌套(W_LAYOUT/W_CARD 递归 + * 子控件会再 push 子控件矩形);顶层 clip=全屏,故 IME/模态直绘不受影响。 */ + meow_gfx_clip_push(w->x, w->y, w->w, w->h); + const meow_theme *th = meow_theme_get(); + int disabled = widget_disabled(w); + + switch (w->type) { + case W_BTN: { + /* 按下/按住 = 视觉态(不改 on_tap/btn_pressed/hit 语义)。按下来源三路: + * - 状态机 MEOW_WS_PRESSED(App 经 set_state 或 SDK 触摸层置位); + * - w->pressed(既有字面,保留); + * - 实时触摸命中:SDK 输入层当前正按在该控件内(无需 app 追踪)。 + * 按住=实时触摸命中且持续 ≥MEOW_BTN_HOLD_MS;按下=任一来源。禁用时不叠按下底。 */ + int live = meow_input_touch_down() && + meow_input_touch_x() >= w->x && meow_input_touch_x() < w->x + w->w && + meow_input_touch_y() >= w->y && meow_input_touch_y() < w->y + w->h; + int press = (w->state & MEOW_WS_PRESSED) || w->pressed || live; + int hold = live && meow_input_touch_hold_ms() >= (unsigned)meow_ui_long_press_ms(); + if (disabled) press = 0; + /* §5.8.6 五型:filled=primary/onPrimary;tonal=secondaryContainer/onSecondaryContainer; + * outlined=透明底+primary 文+outline 描边;text=透明底+primary 文;elevated=surfaceContainerLow+primary 文。 + * 选中态优先生效(primaryContainer / onPrimaryContainer);opts.bg/fg 可覆盖。 */ + int selected = (w->state & MEOW_WS_SELECTED); + int variant = w->variant; + if (variant < MEOW_BTN_FILLED || variant > MEOW_BTN_ELEVATED) variant = MEOW_BTN_FILLED; + int fill = 1, stroke_draw = 0, stroke_w = 1; + uint32_t base, default_fg, stroke_col = 0; + if (selected && !(w->opts_used && w->opts.bg)) { + base = th->primaryContainer; default_fg = th->onPrimaryContainer; + } else { + switch (variant) { + case MEOW_BTN_TONAL: + base = th->secondaryContainer; default_fg = th->onSecondaryContainer; break; + case MEOW_BTN_OUTLINED: + fill = 0; base = 0; default_fg = th->primary; + stroke_draw = 1; stroke_col = th->outline; stroke_w = 1; break; + case MEOW_BTN_TEXT: + fill = 0; base = 0; default_fg = th->primary; break; + case MEOW_BTN_ELEVATED: + base = th->surfaceContainerLow; default_fg = th->primary; + stroke_draw = 1; stroke_col = th->outlineVariant; stroke_w = 1; break; + default: + base = th->primary; default_fg = th->onPrimary; break; + } + } + if (w->opts_used && w->opts.bg) { base = w->opts.bg; fill = 1; } + if (w->opts_used && w->opts.fg) default_fg = w->opts.fg; + uint32_t bg; + if (fill) bg = (hold) ? state_tint(base, th, 24) : (press ? state_tint(base, th, 12) : base); + else bg = 0; + int r = widget_radius(w, 10); + if (fill) meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, bg); + else if (press) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->primary, 12); /* transparent ripple */ + /* 内容区受 pad 垂直内缩(水平居中文本,pad_x 不影响中心) */ + int pady = widget_pady(w); + int cx = w->x + w->w / 2; + int cy = w->y + pady + (w->h - 2 * pady - meow_ui_text_height(th->font_body)) / 2; + uint32_t tc = default_fg; + meow_ui_text_center(cx, cy, w->label, th->font_body, tc); + /* opts 描边(优先)/ 变体内建描边 / 焦点外圈 */ + int sw = (w->opts_used && w->opts.stroke && w->opts.stroke_w > 0) ? w->opts.stroke_w : (stroke_draw ? stroke_w : 0); + uint32_t sc = (w->opts_used && w->opts.stroke) ? w->opts.stroke : stroke_col; + if (sw > 0 && sc) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, sc); + if (w->state & MEOW_WS_FOCUSED) + meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); + /* 禁用:整体变暗(圆角 α,防在圆角外叠出灰方块——M2.71-bug4) */ + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, th->onSurface, 90); + break; + } + case W_SLIDER: { + /* 语义色:轨道=onSurfaceVariant(dim)、填充=primary(accent)、滑块=fg(onSurface)。 */ + int trace_y = w->y + w->h / 2 - 3; + meow_ui_fill_rect(w->x, trace_y, w->w, 6, th->onSurfaceVariant); + int range = w->max - w->min; + int frac = range ? (w->val - w->min) * w->w / range : 0; + if (frac > 0) meow_ui_fill_rect(w->x, trace_y, frac, 6, th->primary); + int tx = w->x + frac; + int ty = w->y + w->h / 2; + meow_gfx_fill_round(tx - 10, ty - 10, 20, 20, 10, th->onSurface); + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_SWITCH: { + uint32_t bg = w->on ? th->primary : th->onSurfaceVariant; + meow_gfx_fill_round(w->x, w->y, w->w, w->h, w->h / 2, bg); + int knob_r = w->h / 2 - 3; + int kx = w->on ? w->x + w->w - knob_r * 2 - 3 : w->x + 3; + meow_gfx_fill_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, 0xFFFFFF); + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->h / 2, th->onSurface, 90); + break; + } + case W_LIST: { + /* §5.8.6 列表容器(LazyColumn/LazyRow):只画可见项(虚拟化)+ 滚动偏移。 + * 纵向=行高 item_h(默认 44,兼容既有 meow_list_create);横向=LazyRow(列宽=item_h)。 */ + int pad = 16; + int item = list_item(w); + int vp = w->horizontal ? w->w : w->h; + int off = w->scroll; + if (off < 0) off = 0; + int maxs = w->count * item - vp; /* max_scroll(与 meow_list_max_scroll 同式) */ + if (maxs < 0) maxs = 0; + if (off > maxs) off = maxs; + int first = (item > 0) ? off / item : 0; + int last = (item > 0) ? (off + vp) / item + 1 : 0; + if (first < 0) first = 0; + if (last > w->count) last = w->count; + /* 视口绝对边界:列表只在其内绘制,杜绝越界泄漏(部分行滚出视口时把填充/分隔线/文本 + * 画到视口外 → 标脏并上屏 → 标题栏/底部残留脏区,P3)。 */ + int vx0 = w->x, vy0 = w->y, vx1 = w->x + w->w, vy1 = w->y + w->h; + int thh = meow_ui_text_height(th->font_body); + for (int i = first; i < last; i++) { + char label[64] = ""; + if (!w->src || !w->src(i, label, (int)sizeof label, w->ctx)) continue; + int pos = i * item - off; /* 相对容器起点的可视位置 */ + if (w->horizontal) { + int rx = w->x + pos; + if (rx >= vx1) break; /* 完全在右缘外 */ + if (rx + item <= vx0) continue; /* 完全在左缘外(滚出残余) */ + int cl = rx > vx0 ? rx : vx0; /* 可见左界 */ + int cr = (rx + item) < vx1 ? (rx + item) : vx1; /* 可见右界 */ + if (i == w->sel) meow_ui_fill_rect(cl, w->y, cr - cl, w->h, th->primary); + uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface; + int ty = w->y + (w->h - thh) / 2; + if (rx >= vx0 && rx + item <= vx1) /* 仅整项可见才画文本(防泄漏) */ + meow_ui_text_center(rx + item / 2, ty, label, th->font_body, col); + int sxp = rx + item - 1; /* 分隔线:仅当落在可见区 */ + if (sxp >= cl && sxp < cr) meow_ui_fill_rect(sxp, w->y, 1, w->h, th->onSurfaceVariant); + } else { + int ry = w->y + pos; + if (ry >= vy1) break; /* 完全在底缘外 */ + if (ry + item <= vy0) continue; /* 完全在顶缘外(滚出残余) */ + int cl = ry > vy0 ? ry : vy0; /* 可见顶界 */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ + if (i == w->sel) meow_ui_fill_rect(w->x, cl, w->w, cr - cl, th->primary); + uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface; + int ty = ry + (item - thh) / 2; + if (ty >= vy0 && ty + thh <= vy1) /* 文本不越界才画(防标题/底部泄漏) */ + meow_ui_text(w->x + pad, ty, label, th->font_body, col); + int sy = ry + item - 1; /* 分隔线:仅当落在可见区 */ + if (sy >= cl && sy < cr) meow_ui_fill_rect(w->x, sy, w->w, 1, th->onSurfaceVariant); + } + } + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_LISTVIEW: { + /* §5.5 结构化列表(纵向 LazyColumn):只画可见行(虚拟化)+ 视口裁剪(M2.73 纪律)。 + * 行 = 图标(左) + 标题 + 副标题(次要色) + 右侧操作区;选中行 = primaryContainer / + * onPrimaryContainer(§5.8.3);禁用行变淡不响应(命中由 hit 层保证)。全语义色。 */ + int item = listv_item_h(w); + int vp = w->h; /* 纵向 */ + int off = w->scroll; + int maxs = w->count * item - vp; + if (maxs < 0) maxs = 0; + if (off < 0) off = 0; + if (off > maxs) off = maxs; + int first = (item > 0) ? off / item : 0; + int last = (item > 0) ? (off + vp) / item + 1 : 0; + if (first < 0) first = 0; + if (last > w->count) last = w->count; + int vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h; + int thh = meow_ui_text_height(th->font_body); + int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON; + int text_x = vx0 + pad + isz + pad; + int op_start = vx0 + (w->w * 2 / 3); /* 行右 1/3 = 操作区(§5.5 命中) */ + int text_maxw = op_start - pad - text_x; /* 标题/副标题可用宽(不压操作区) */ + if (text_maxw < 8) text_maxw = 8; + for (int i = first; i < last; i++) { + meow_list_item it; + memset(&it, 0, sizeof it); + if (!w->src2 || !w->src2(i, &it, w->ctx)) continue; + int ry = w->y + (i * item - off); + if (ry >= vy1) break; + if (ry + item <= vy0) continue; + int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */ + int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */ + int sel = (i == w->sel); + uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface; + uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant; + if (sel) meow_ui_fill_rect(vx0, cl, w->w, cr - cl, th->primaryContainer); + + /* 图标(左) */ + uint32_t iccol = sel ? th->onPrimaryContainer + : ((it.icon_kind == MEOW_LIST_ICON_FOLDER) ? th->primary + : th->onSurfaceVariant); + int icy = ry + (item - isz) / 2; + if (icy + isz >= vy0 && icy < vy1) /* 图标不越视口边界才画(防泄漏) */ + listv_draw_icon(th, vx0 + pad, icy, it.icon_kind, it.icon, iccol); + + /* 标题 + 副标题(两行居中;无副标题=单行居中;越界不画防泄漏) */ + int ty; + if (it.subtitle && it.subtitle[0]) { + ty = ry + (item - (2 * thh + MEOW_LISTV_SUB_GAP)) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + int sy = ty + thh + MEOW_LISTV_SUB_GAP; + if (sy >= vy0 && sy + thh <= vy1) + listv_text_trunc(text_x, sy, it.subtitle, text_maxw, th->font_body, subcol); + } else { + ty = ry + (item - thh) / 2; + if (it.title && ty >= vy0 && ty + thh <= vy1) + listv_text_trunc(text_x, ty, it.title, text_maxw, th->font_body, textcol); + } + + /* 右侧操作区 */ + if (it.right != MEOW_LIST_RIGHT_NONE) { + int ax = w->x + w->w - pad - 24; /* 右缘动作槽(约 24px) */ + if (ax >= op_start) + listv_draw_right(th, ax, ry, item, it.right, it.on, subcol); + } + + /* 分隔线(仅落在可见带内才画) */ + int sy = ry + item - 1; + if (sy >= cl && sy < cr) + meow_ui_fill_rect(vx0, sy, w->w, 1, th->outlineVariant); + + /* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */ + if (!it.enabled) + meow_ui_fill_rect_alpha(vx0, cl, w->w, cr - cl, th->onSurface, 90); + } + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_CUSTOM: + /* SDK 不替自定义画布画背景/状态层;App 在 render_cb 里自绘(th 供取色), + * 可读 meow_widget_state(状态位)/ meow_widget_opts_get(通用修饰)自行叠。 */ + if (w->render_cb) w->render_cb(w, th, w->ctx); + break; + case W_LAYOUT: + /* 递归绘制子控件(子控件坐标已由 meow_layout_arrange 算成绝对坐标)。 */ + for (int i = 0; i < w->nchildren; i++) + if (w->children[i]) meow_widget_draw(w->children[i]); + break; + case W_TEXTFIELD: { + /* MD3 输入框:圆角外框(surfaceContainerLowest 底 + outline 描边;focused=primary 2px)+ 提示/文本 + 光标。 */ + int r = widget_radius(w, 8); + int focused = (w->state & MEOW_WS_FOCUSED) != 0; + meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, th->surfaceContainerLowest); + uint32_t bcol = focused ? th->primary : th->outline; + int bw = focused ? 2 : 1; + if (bw >= 2) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, bcol); + else meow_ui_draw_rect_round(w->x + 1, w->y + 1, w->w - 2, w->h - 2, + r > 1 ? r - 1 : 0, bcol); + int has = w->buf && w->buf[0]; + uint32_t col = has ? th->onSurface : th->onSurfaceVariant; + const char *s = has ? w->buf : (w->placeholder[0] ? w->placeholder : ""); + int tx = w->x + 12; + int ty = w->y + (w->h - meow_ui_text_height(th->font_body)) / 2; + meow_ui_text(tx, ty, s, th->font_body, col); + if (focused && has) { + int tw = meow_ui_text_width(w->buf, th->font_body); + meow_ui_fill_rect(tx + tw, ty, 2, meow_ui_text_height(th->font_body), th->primary); + } + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, th->onSurface, 90); + break; + } + case W_CARD: { + int r = widget_radius(w, th->radius_card); + uint32_t base = widget_bg(w, th, th->surfaceContainerLow); + meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, base); + if (w->opts_used && w->opts.stroke && w->opts.stroke_w > 0) + meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, w->opts.stroke); + if (w->state & MEOW_WS_SELECTED) + meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->primary, 24); + if (w->nchildren > 0 && w->children[0]) meow_widget_draw(w->children[0]); + if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, th->onSurface, 90); + break; + } + case W_DIVIDER: { + uint32_t c = widget_bg(w, th, th->outlineVariant); + if (w->variant) meow_ui_fill_rect(w->x, w->y, 1, w->h, c); + else meow_ui_fill_rect(w->x, w->y, w->w, 1, c); + break; + } + case W_BADGE: { + int count = w->value; + if (count < 0) break; /* 隐藏 */ + uint32_t bgc = th->error; /* MD3 角标用 error */ + if (count == 0) { + meow_gfx_fill_circle(w->x, w->y, 5, bgc); + } else { + char s[16]; + snprintf(s, sizeof s, "%d", count); + int tw = meow_ui_text_width(s, th->font_body); + int pw = tw + 12; if (pw < 18) pw = 18; + meow_gfx_fill_round(w->x - pw / 2, w->y - 11, pw, 22, 11, bgc); + meow_ui_text_center(w->x, w->y - meow_ui_text_height(th->font_body) / 2, s, th->font_body, th->onError); + } + break; + } + case W_CHECKBOX: { + int sel = (w->state & MEOW_WS_SELECTED) != 0; + int sz = w->w; + int r = widget_radius(w, 4); + if (sel) { + meow_gfx_fill_round(w->x, w->y, sz, sz, r, th->primary); + int ax = w->x + 9, ay1 = w->y + 17, ay2 = w->y + 23, bx = w->x + 15, cx = w->x + 25, cy = w->y + 11; + meow_gfx_line(ax, ay1, bx, ay2, 3, th->onPrimary); + meow_gfx_line(bx, ay2, cx, cy, 3, th->onPrimary); + } else { + meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->outline); + } + if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->primary); + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, sz, sz, r, th->onSurface, 90); + break; + } + case W_RADIO: { + int sel = (w->state & MEOW_WS_SELECTED) != 0; + int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2; + meow_gfx_ring_arc(ccx, ccy, 12, 2, 0, 360, sel ? th->primary : th->outline); + if (sel) meow_gfx_fill_circle(ccx, ccy, 6, th->primary); + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->w / 2, th->onSurface, 90); + break; + } + case W_PROGRESS: { + if (w->variant == 1) { /* 圆形(几何 = 整圆像素外接框;由此解回圆心/半径) */ + int thk = MEOW_PROGRESS_ARC_THK; + int m = MEOW_PROGRESS_ARC_MARGIN; + int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2; + int rr = w->w / 2 - m; /* 与 create 一致:回到调用方传入的半径 r */ + if (rr <= 0) break; + meow_gfx_ring_arc(ccx, ccy, rr, thk, 0, 360, th->surfaceVariant); + if (w->value < 0) { /* 不定:按时间推进一段弧(App 在 on_tick 里重绘驱动) */ + int start = (int)((meow_ui_time_ms() / 30) % 360); + meow_gfx_ring_arc(ccx, ccy, rr, thk, start - 90, 90, th->primary); + } else if (w->value > 0) { + int sweep = w->value * 360 / 100; + if (sweep > 0) meow_gfx_ring_arc(ccx, ccy, rr, thk, -90, sweep, th->primary); + } + } else { /* 线性 */ + int thk = w->h > 0 ? w->h : 4; + int yy = w->y + w->h / 2 - thk / 2; + meow_ui_fill_rect(w->x, yy, w->w, thk, th->surfaceVariant); + if (w->value < 0) { /* 不定:移动块 */ + int fw = w->w / 3; + int pos = (int)((meow_ui_time_ms() / 12) % (w->w + fw)) - fw; + meow_ui_fill_rect(w->x + pos, yy, fw, thk, th->primary); + } else if (w->value > 0) { + int fw = w->value * w->w / 100; + if (fw > 0) meow_ui_fill_rect(w->x, yy, fw, thk, th->primary); + } + } + if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90); + break; + } + case W_FAB: { + int r = widget_radius(w, w->w / 2); + uint32_t base = widget_bg(w, th, th->primaryContainer); + if (w->state & MEOW_WS_PRESSED) base = state_tint(base, th, 12); + meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, base); + uint32_t tc = widget_fg(w, th, th->onPrimaryContainer); + meow_ui_text_center(w->x + w->w / 2, w->y + (w->h - meow_ui_text_height(th->font_body)) / 2, + w->label, th->font_body, tc); + if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, w->w, w->h, r, th->primary); + if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, r, th->onSurface, 90); + break; + } + case W_TOPAPPBAR: { + meow_ui_fill_rect(w->x, w->y, w->w, w->h, th->surfaceContainerHigh); + int mid = w->y + w->h / 2; + int tx = w->x + 16; + int ty = mid - meow_ui_text_height(th->font_title) / 2; + if (w->back_on) { + int bx = w->x + 22, by = mid; + meow_gfx_line(bx + 4, by - 7, bx - 4, by, 2, th->onSurface); + meow_gfx_line(bx - 4, by, bx + 4, by + 7, 2, th->onSurface); + tx = w->x + 48; + } + if (w->label[0]) meow_ui_text(tx, ty, w->label, th->font_title, th->onSurface); + int ay = mid - meow_ui_text_height(th->font_body) / 2; + int ax = w->x + w->w - 12; + for (int i = w->nitems - 1; i >= 0; i--) { + int aw = meow_ui_text_width(w->items[i], th->font_body) + 12; + ax -= aw; + meow_ui_text_center(ax + aw / 2, ay, w->items[i], th->font_body, th->onSurface); + } + meow_ui_fill_rect(w->x, w->y + w->h - 1, w->w, 1, th->outlineVariant); + break; + } + case W_NAVBAR: { + meow_ui_fill_rect(w->x, w->y, w->w, w->h, th->surfaceContainer); + if (w->nitems <= 0) break; + int iw = w->w / w->nitems; + int ay = w->y + (w->h - meow_ui_text_height(th->font_body)) / 2 + 12; + for (int i = 0; i < w->nitems; i++) { + int ix = w->x + i * iw; + int sel = (i == w->selected); + if (sel) meow_gfx_fill_round(ix + iw / 2 - 24, w->y + 8, 48, 22, 11, th->secondaryContainer); + uint32_t col = sel ? th->onSecondaryContainer : th->onSurfaceVariant; + meow_ui_text_center(ix + iw / 2, ay, w->items[i], th->font_body, col); + } + meow_ui_fill_rect(w->x, w->y, w->w, 1, th->outlineVariant); + break; + } + case W_TABBAR: { + meow_ui_fill_rect(w->x, w->y, w->w, w->h, th->surface); + if (w->nitems <= 0) break; + int iw = w->w / w->nitems; + int ay = w->y + (w->h - meow_ui_text_height(th->font_body)) / 2; + for (int i = 0; i < w->nitems; i++) { + int ix = w->x + i * iw; + int sel = (i == w->selected); + meow_ui_text_center(ix + iw / 2, ay, w->items[i], th->font_body, + sel ? th->primary : th->onSurfaceVariant); + if (sel) meow_ui_fill_rect(ix, w->y + w->h - 2, iw, 2, th->primary); + } + meow_ui_fill_rect(w->x, w->y + w->h - 1, w->w, 1, th->outlineVariant); + break; + } + case W_SPACER: + break; /* 不可见空白 */ + default: + break; + } + meow_gfx_clip_pop(); /* 恢复进入前的 clip(嵌套:回到父控件矩形/全屏) */ +} diff --git a/sdk/render_sb_statusbar.c b/sdk/render_sb_statusbar.c new file mode 100644 index 0000000..90e213b --- /dev/null +++ b/sdk/render_sb_statusbar.c @@ -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 +#include +#include + +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; +} diff --git a/sdk/sb_icons.h b/sdk/sb_icons.h new file mode 100644 index 0000000..db7570f --- /dev/null +++ b/sdk/sb_icons.h @@ -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 */ \ No newline at end of file diff --git a/sdk/stb_image.c b/sdk/stb_image.c new file mode 100644 index 0000000..c9fa25b --- /dev/null +++ b/sdk/stb_image.c @@ -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" diff --git a/sdk/stb_image.h b/sdk/stb_image.h new file mode 100644 index 0000000..9eedabe --- /dev/null +++ b/sdk/stb_image.h @@ -0,0 +1,7988 @@ +/* stb_image - v2.30 - public domain image loader - http://nothings.org/stb + no warranty implied; use at your own risk + + Do this: + #define STB_IMAGE_IMPLEMENTATION + before you include this file in *one* C or C++ file to create the implementation. + + // i.e. it should look like this: + #include ... + #include ... + #include ... + #define STB_IMAGE_IMPLEMENTATION + #include "stb_image.h" + + You can #define STBI_ASSERT(x) before the #include to avoid using assert.h. + And #define STBI_MALLOC, STBI_REALLOC, and STBI_FREE to avoid using malloc,realloc,free + + + QUICK NOTES: + Primarily of interest to game developers and other people who can + avoid problematic images and only need the trivial interface + + JPEG baseline & progressive (12 bpc/arithmetic not supported, same as stock IJG lib) + PNG 1/2/4/8/16-bit-per-channel + + TGA (not sure what subset, if a subset) + BMP non-1bpp, non-RLE + PSD (composited view only, no extra channels, 8/16 bit-per-channel) + + GIF (*comp always reports as 4-channel) + HDR (radiance rgbE format) + PIC (Softimage PIC) + PNM (PPM and PGM binary only) + + Animated GIF still needs a proper API, but here's one way to do it: + http://gist.github.com/urraka/685d9a6340b26b830d49 + + - decode from memory or through FILE (define STBI_NO_STDIO to remove code) + - decode from arbitrary I/O callbacks + - SIMD acceleration on x86/x64 (SSE2) and ARM (NEON) + + Full documentation under "DOCUMENTATION" below. + + +LICENSE + + See end of file for license information. + +RECENT REVISION HISTORY: + + 2.30 (2024-05-31) avoid erroneous gcc warning + 2.29 (2023-05-xx) optimizations + 2.28 (2023-01-29) many error fixes, security errors, just tons of stuff + 2.27 (2021-07-11) document stbi_info better, 16-bit PNM support, bug fixes + 2.26 (2020-07-13) many minor fixes + 2.25 (2020-02-02) fix warnings + 2.24 (2020-02-02) fix warnings; thread-local failure_reason and flip_vertically + 2.23 (2019-08-11) fix clang static analysis warning + 2.22 (2019-03-04) gif fixes, fix warnings + 2.21 (2019-02-25) fix typo in comment + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs + 2.19 (2018-02-11) fix warning + 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) bugfix, 1-bit BMP, 16-bitness query, fix warnings + 2.16 (2017-07-23) all functions have 16-bit variants; optimizations; bugfixes + 2.15 (2017-03-18) fix png-1,2,4; all Imagenet JPGs; no runtime SSE detection on GCC + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs + 2.13 (2016-12-04) experimental 16-bit API, only for PNG so far; fixes + 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes + 2.11 (2016-04-02) 16-bit PNGS; enable SSE2 in non-gcc x64 + RGB-format JPEG; remove white matting in PSD; + allocate large structures on the stack; + correct channel count for PNG & BMP + 2.10 (2016-01-22) avoid warning introduced in 2.09 + 2.09 (2016-01-16) 16-bit TGA; comments in PNM files; STBI_REALLOC_SIZED + + See end of file for full revision history. + + + ============================ Contributors ========================= + + Image formats Extensions, features + Sean Barrett (jpeg, png, bmp) Jetro Lauha (stbi_info) + Nicolas Schulz (hdr, psd) Martin "SpartanJ" Golini (stbi_info) + Jonathan Dummer (tga) James "moose2000" Brown (iPhone PNG) + Jean-Marc Lienher (gif) Ben "Disch" Wenger (io callbacks) + Tom Seddon (pic) Omar Cornut (1/2/4-bit PNG) + Thatcher Ulrich (psd) Nicolas Guillemot (vertical flip) + Ken Miller (pgm, ppm) Richard Mitton (16-bit PSD) + github:urraka (animated gif) Junggon Kim (PNM comments) + Christopher Forseth (animated gif) Daniel Gibson (16-bit TGA) + socks-the-fox (16-bit PNG) + Jeremy Sawicki (handle all ImageNet JPGs) + Optimizations & bugfixes Mikhail Morozov (1-bit BMP) + Fabian "ryg" Giesen Anael Seghezzi (is-16-bit query) + Arseny Kapoulkine Simon Breuss (16-bit PNM) + John-Mark Allen + Carmelo J Fdez-Aguera + + Bug & warning fixes + Marc LeBlanc David Woo Guillaume George Martins Mozeiko + Christpher Lloyd Jerry Jansson Joseph Thomson Blazej Dariusz Roszkowski + Phil Jordan Dave Moore Roy Eltham + Hayaki Saito Nathan Reed Won Chun + Luke Graham Johan Duparc Nick Verigakis the Horde3D community + Thomas Ruf Ronny Chevalier github:rlyeh + Janez Zemva John Bartholomew Michal Cichon github:romigrou + Jonathan Blow Ken Hamada Tero Hanninen github:svdijk + Eugene Golushkov Laurent Gomila Cort Stratton github:snagar + Aruelien Pocheville Sergio Gonzalez Thibault Reuille github:Zelex + Cass Everitt Ryamond Barbiero github:grim210 + Paul Du Bois Engin Manap Aldo Culquicondor github:sammyhw + Philipp Wiesemann Dale Weiler Oriol Ferrer Mesia github:phprus + Josh Tobin Neil Bickford Matthew Gregan github:poppolopoppo + Julian Raschke Gregory Mullen Christian Floisand github:darealshinji + Baldur Karlsson Kevin Schmidt JR Smith github:Michaelangel007 + Brad Weinberger Matvey Cherevko github:mosra + Luca Sas Alexander Veselov Zack Middleton [reserved] + Ryan C. Gordon [reserved] [reserved] + DO NOT ADD YOUR NAME HERE + + Jacko Dirks + + To add your name to the credits, pick a random blank space in the middle and fill it. + 80% of merge conflicts on stb PRs are due to people adding their name at the end + of the credits. +*/ + +#ifndef STBI_INCLUDE_STB_IMAGE_H +#define STBI_INCLUDE_STB_IMAGE_H + +// DOCUMENTATION +// +// Limitations: +// - no 12-bit-per-channel JPEG +// - no JPEGs with arithmetic coding +// - GIF always returns *comp=4 +// +// Basic usage (see HDR discussion below for HDR usage): +// int x,y,n; +// unsigned char *data = stbi_load(filename, &x, &y, &n, 0); +// // ... process data if not NULL ... +// // ... x = width, y = height, n = # 8-bit components per pixel ... +// // ... replace '0' with '1'..'4' to force that many components per pixel +// // ... but 'n' will always be the number that it would have been if you said 0 +// stbi_image_free(data); +// +// Standard parameters: +// int *x -- outputs image width in pixels +// int *y -- outputs image height in pixels +// int *channels_in_file -- outputs # of image components in image file +// int desired_channels -- if non-zero, # of image components requested in result +// +// The return value from an image loader is an 'unsigned char *' which points +// to the pixel data, or NULL on an allocation failure or if the image is +// corrupt or invalid. The pixel data consists of *y scanlines of *x pixels, +// with each pixel consisting of N interleaved 8-bit components; the first +// pixel pointed to is top-left-most in the image. There is no padding between +// image scanlines or between pixels, regardless of format. The number of +// components N is 'desired_channels' if desired_channels is non-zero, or +// *channels_in_file otherwise. If desired_channels is non-zero, +// *channels_in_file has the number of components that _would_ have been +// output otherwise. E.g. if you set desired_channels to 4, you will always +// get RGBA output, but you can check *channels_in_file to see if it's trivially +// opaque because e.g. there were only 3 channels in the source image. +// +// An output image with N components has the following components interleaved +// in this order in each pixel: +// +// N=#comp components +// 1 grey +// 2 grey, alpha +// 3 red, green, blue +// 4 red, green, blue, alpha +// +// If image loading fails for any reason, the return value will be NULL, +// and *x, *y, *channels_in_file will be unchanged. The function +// stbi_failure_reason() can be queried for an extremely brief, end-user +// unfriendly explanation of why the load failed. Define STBI_NO_FAILURE_STRINGS +// to avoid compiling these strings at all, and STBI_FAILURE_USERMSG to get slightly +// more user-friendly ones. +// +// Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. +// +// To query the width, height and component count of an image without having to +// decode the full file, you can use the stbi_info family of functions: +// +// int x,y,n,ok; +// ok = stbi_info(filename, &x, &y, &n); +// // returns ok=1 and sets x, y, n if image is a supported format, +// // 0 otherwise. +// +// Note that stb_image pervasively uses ints in its public API for sizes, +// including sizes of memory buffers. This is now part of the API and thus +// hard to change without causing breakage. As a result, the various image +// loaders all have certain limits on image size; these differ somewhat +// by format but generally boil down to either just under 2GB or just under +// 1GB. When the decoded image would be larger than this, stb_image decoding +// will fail. +// +// Additionally, stb_image will reject image files that have any of their +// dimensions set to a larger value than the configurable STBI_MAX_DIMENSIONS, +// which defaults to 2**24 = 16777216 pixels. Due to the above memory limit, +// the only way to have an image with such dimensions load correctly +// is for it to have a rather extreme aspect ratio. Either way, the +// assumption here is that such larger images are likely to be malformed +// or malicious. If you do need to load an image with individual dimensions +// larger than that, and it still fits in the overall size limit, you can +// #define STBI_MAX_DIMENSIONS on your own to be something larger. +// +// =========================================================================== +// +// UNICODE: +// +// If compiling for Windows and you wish to use Unicode filenames, compile +// with +// #define STBI_WINDOWS_UTF8 +// and pass utf8-encoded filenames. Call stbi_convert_wchar_to_utf8 to convert +// Windows wchar_t filenames to utf8. +// +// =========================================================================== +// +// Philosophy +// +// stb libraries are designed with the following priorities: +// +// 1. easy to use +// 2. easy to maintain +// 3. good performance +// +// Sometimes I let "good performance" creep up in priority over "easy to maintain", +// and for best performance I may provide less-easy-to-use APIs that give higher +// performance, in addition to the easy-to-use ones. Nevertheless, it's important +// to keep in mind that from the standpoint of you, a client of this library, +// all you care about is #1 and #3, and stb libraries DO NOT emphasize #3 above all. +// +// Some secondary priorities arise directly from the first two, some of which +// provide more explicit reasons why performance can't be emphasized. +// +// - Portable ("ease of use") +// - Small source code footprint ("easy to maintain") +// - No dependencies ("ease of use") +// +// =========================================================================== +// +// I/O callbacks +// +// I/O callbacks allow you to read from arbitrary sources, like packaged +// files or some other source. Data read from callbacks are processed +// through a small internal buffer (currently 128 bytes) to try to reduce +// overhead. +// +// The three functions you must define are "read" (reads some bytes of data), +// "skip" (skips some bytes of data), "eof" (reports if the stream is at the end). +// +// =========================================================================== +// +// SIMD support +// +// The JPEG decoder will try to automatically use SIMD kernels on x86 when +// supported by the compiler. For ARM Neon support, you must explicitly +// request it. +// +// (The old do-it-yourself SIMD API is no longer supported in the current +// code.) +// +// On x86, SSE2 will automatically be used when available based on a run-time +// test; if not, the generic C versions are used as a fall-back. On ARM targets, +// the typical path is to have separate builds for NEON and non-NEON devices +// (at least this is true for iOS and Android). Therefore, the NEON support is +// toggled by a build flag: define STBI_NEON to get NEON loops. +// +// If for some reason you do not want to use any of SIMD code, or if +// you have issues compiling it, you can disable it entirely by +// defining STBI_NO_SIMD. +// +// =========================================================================== +// +// HDR image support (disable by defining STBI_NO_HDR) +// +// stb_image supports loading HDR images in general, and currently the Radiance +// .HDR file format specifically. You can still load any file through the existing +// interface; if you attempt to load an HDR file, it will be automatically remapped +// to LDR, assuming gamma 2.2 and an arbitrary scale factor defaulting to 1; +// both of these constants can be reconfigured through this interface: +// +// stbi_hdr_to_ldr_gamma(2.2f); +// stbi_hdr_to_ldr_scale(1.0f); +// +// (note, do not use _inverse_ constants; stbi_image will invert them +// appropriately). +// +// Additionally, there is a new, parallel interface for loading files as +// (linear) floats to preserve the full dynamic range: +// +// float *data = stbi_loadf(filename, &x, &y, &n, 0); +// +// If you load LDR images through this interface, those images will +// be promoted to floating point values, run through the inverse of +// constants corresponding to the above: +// +// stbi_ldr_to_hdr_scale(1.0f); +// stbi_ldr_to_hdr_gamma(2.2f); +// +// Finally, given a filename (or an open file or memory block--see header +// file for details) containing image data, you can query for the "most +// appropriate" interface to use (that is, whether the image is HDR or +// not), using: +// +// stbi_is_hdr(char *filename); +// +// =========================================================================== +// +// iPhone PNG support: +// +// We optionally support converting iPhone-formatted PNGs (which store +// premultiplied BGRA) back to RGB, even though they're internally encoded +// differently. To enable this conversion, call +// stbi_convert_iphone_png_to_rgb(1). +// +// Call stbi_set_unpremultiply_on_load(1) as well to force a divide per +// pixel to remove any premultiplied alpha *only* if the image file explicitly +// says there's premultiplied data (currently only happens in iPhone images, +// and only if iPhone convert-to-rgb processing is on). +// +// =========================================================================== +// +// ADDITIONAL CONFIGURATION +// +// - You can suppress implementation of any of the decoders to reduce +// your code footprint by #defining one or more of the following +// symbols before creating the implementation. +// +// STBI_NO_JPEG +// STBI_NO_PNG +// STBI_NO_BMP +// STBI_NO_PSD +// STBI_NO_TGA +// STBI_NO_GIF +// STBI_NO_HDR +// STBI_NO_PIC +// STBI_NO_PNM (.ppm and .pgm) +// +// - You can request *only* certain decoders and suppress all other ones +// (this will be more forward-compatible, as addition of new decoders +// doesn't require you to disable them explicitly): +// +// STBI_ONLY_JPEG +// STBI_ONLY_PNG +// STBI_ONLY_BMP +// STBI_ONLY_PSD +// STBI_ONLY_TGA +// STBI_ONLY_GIF +// STBI_ONLY_HDR +// STBI_ONLY_PIC +// STBI_ONLY_PNM (.ppm and .pgm) +// +// - If you use STBI_NO_PNG (or _ONLY_ without PNG), and you still +// want the zlib decoder to be available, #define STBI_SUPPORT_ZLIB +// +// - If you define STBI_MAX_DIMENSIONS, stb_image will reject images greater +// than that size (in either width or height) without further processing. +// This is to let programs in the wild set an upper bound to prevent +// denial-of-service attacks on untrusted data, as one could generate a +// valid image of gigantic dimensions and force stb_image to allocate a +// huge block of memory and spend disproportionate time decoding it. By +// default this is set to (1 << 24), which is 16777216, but that's still +// very big. + +#ifndef STBI_NO_STDIO +#include +#endif // STBI_NO_STDIO + +#define STBI_VERSION 1 + +enum +{ + STBI_default = 0, // only used for desired_channels + + STBI_grey = 1, + STBI_grey_alpha = 2, + STBI_rgb = 3, + STBI_rgb_alpha = 4 +}; + +#include +typedef unsigned char stbi_uc; +typedef unsigned short stbi_us; + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef STBIDEF +#ifdef STB_IMAGE_STATIC +#define STBIDEF static +#else +#define STBIDEF extern +#endif +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// PRIMARY API - works on images of any type +// + +// +// load image by filename, open file, or memory buffer +// + +typedef struct +{ + int (*read) (void *user,char *data,int size); // fill 'data' with 'size' bytes. return number of bytes actually read + void (*skip) (void *user,int n); // skip the next 'n' bytes, or 'unget' the last -n bytes if negative + int (*eof) (void *user); // returns nonzero if we are at end of file/data +} stbi_io_callbacks; + +//////////////////////////////////// +// +// 8-bits-per-channel interface +// + +STBIDEF stbi_uc *stbi_load_from_memory (stbi_uc const *buffer, int len , int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk , void *user, int *x, int *y, int *channels_in_file, int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_uc *stbi_load (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); +// for stbi_load_from_file, file pointer is left pointing immediately after image +#endif + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp); +#endif + +#ifdef STBI_WINDOWS_UTF8 +STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif + +//////////////////////////////////// +// +// 16-bits-per-channel interface +// + +STBIDEF stbi_us *stbi_load_16_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); + +#ifndef STBI_NO_STDIO +STBIDEF stbi_us *stbi_load_16 (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); +STBIDEF stbi_us *stbi_load_from_file_16(FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); +#endif + +//////////////////////////////////// +// +// float-per-channel interface +// +#ifndef STBI_NO_LINEAR + STBIDEF float *stbi_loadf_from_memory (stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels); + STBIDEF float *stbi_loadf_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels); + + #ifndef STBI_NO_STDIO + STBIDEF float *stbi_loadf (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels); + STBIDEF float *stbi_loadf_from_file (FILE *f, int *x, int *y, int *channels_in_file, int desired_channels); + #endif +#endif + +#ifndef STBI_NO_HDR + STBIDEF void stbi_hdr_to_ldr_gamma(float gamma); + STBIDEF void stbi_hdr_to_ldr_scale(float scale); +#endif // STBI_NO_HDR + +#ifndef STBI_NO_LINEAR + STBIDEF void stbi_ldr_to_hdr_gamma(float gamma); + STBIDEF void stbi_ldr_to_hdr_scale(float scale); +#endif // STBI_NO_LINEAR + +// stbi_is_hdr is always defined, but always returns false if STBI_NO_HDR +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user); +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len); +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr (char const *filename); +STBIDEF int stbi_is_hdr_from_file(FILE *f); +#endif // STBI_NO_STDIO + + +// get a VERY brief reason for failure +// on most compilers (and ALL modern mainstream compilers) this is threadsafe +STBIDEF const char *stbi_failure_reason (void); + +// free the loaded image -- this is just free() +STBIDEF void stbi_image_free (void *retval_from_stbi_load); + +// get image dimensions & components without fully decoding +STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp); +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp); +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len); +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *clbk, void *user); + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info (char const *filename, int *x, int *y, int *comp); +STBIDEF int stbi_info_from_file (FILE *f, int *x, int *y, int *comp); +STBIDEF int stbi_is_16_bit (char const *filename); +STBIDEF int stbi_is_16_bit_from_file(FILE *f); +#endif + + + +// for image formats that explicitly notate that they have premultiplied alpha, +// we just return the colors as stored in the file. set this flag to force +// unpremultiplication. results are undefined if the unpremultiply overflow. +STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); + +// indicate whether we should process iphone images back to canonical format, +// or just pass them through "as-is" +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); + +// flip the image vertically, so the first pixel in the output array is the bottom left +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip); + +// as above, but only applies to images loaded on the thread that calls the function +// this function is only available if your compiler supports thread-local variables; +// calling it will fail to link if your compiler doesn't +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply); +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert); +STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip); + +// ZLIB client - used by PNG, available for other purposes + +STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen); +STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header); +STBIDEF char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen); +STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + +STBIDEF char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen); +STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + + +#ifdef __cplusplus +} +#endif + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBI_INCLUDE_STB_IMAGE_H + +#ifdef STB_IMAGE_IMPLEMENTATION + +#if defined(STBI_ONLY_JPEG) || defined(STBI_ONLY_PNG) || defined(STBI_ONLY_BMP) \ + || defined(STBI_ONLY_TGA) || defined(STBI_ONLY_GIF) || defined(STBI_ONLY_PSD) \ + || defined(STBI_ONLY_HDR) || defined(STBI_ONLY_PIC) || defined(STBI_ONLY_PNM) \ + || defined(STBI_ONLY_ZLIB) + #ifndef STBI_ONLY_JPEG + #define STBI_NO_JPEG + #endif + #ifndef STBI_ONLY_PNG + #define STBI_NO_PNG + #endif + #ifndef STBI_ONLY_BMP + #define STBI_NO_BMP + #endif + #ifndef STBI_ONLY_PSD + #define STBI_NO_PSD + #endif + #ifndef STBI_ONLY_TGA + #define STBI_NO_TGA + #endif + #ifndef STBI_ONLY_GIF + #define STBI_NO_GIF + #endif + #ifndef STBI_ONLY_HDR + #define STBI_NO_HDR + #endif + #ifndef STBI_ONLY_PIC + #define STBI_NO_PIC + #endif + #ifndef STBI_ONLY_PNM + #define STBI_NO_PNM + #endif +#endif + +#if defined(STBI_NO_PNG) && !defined(STBI_SUPPORT_ZLIB) && !defined(STBI_NO_ZLIB) +#define STBI_NO_ZLIB +#endif + + +#include +#include // ptrdiff_t on osx +#include +#include +#include + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) +#include // ldexp, pow +#endif + +#ifndef STBI_NO_STDIO +#include +#endif + +#ifndef STBI_ASSERT +#include +#define STBI_ASSERT(x) assert(x) +#endif + +#ifdef __cplusplus +#define STBI_EXTERN extern "C" +#else +#define STBI_EXTERN extern +#endif + + +#ifndef _MSC_VER + #ifdef __cplusplus + #define stbi_inline inline + #else + #define stbi_inline + #endif +#else + #define stbi_inline __forceinline +#endif + +#ifndef STBI_NO_THREAD_LOCALS + #if defined(__cplusplus) && __cplusplus >= 201103L + #define STBI_THREAD_LOCAL thread_local + #elif defined(__GNUC__) && __GNUC__ < 5 + #define STBI_THREAD_LOCAL __thread + #elif defined(_MSC_VER) + #define STBI_THREAD_LOCAL __declspec(thread) + #elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_THREADS__) + #define STBI_THREAD_LOCAL _Thread_local + #endif + + #ifndef STBI_THREAD_LOCAL + #if defined(__GNUC__) + #define STBI_THREAD_LOCAL __thread + #endif + #endif +#endif + +#if defined(_MSC_VER) || defined(__SYMBIAN32__) +typedef unsigned short stbi__uint16; +typedef signed short stbi__int16; +typedef unsigned int stbi__uint32; +typedef signed int stbi__int32; +#else +#include +typedef uint16_t stbi__uint16; +typedef int16_t stbi__int16; +typedef uint32_t stbi__uint32; +typedef int32_t stbi__int32; +#endif + +// should produce compiler error if size is wrong +typedef unsigned char validate_uint32[sizeof(stbi__uint32)==4 ? 1 : -1]; + +#ifdef _MSC_VER +#define STBI_NOTUSED(v) (void)(v) +#else +#define STBI_NOTUSED(v) (void)sizeof(v) +#endif + +#ifdef _MSC_VER +#define STBI_HAS_LROTL +#endif + +#ifdef STBI_HAS_LROTL + #define stbi_lrot(x,y) _lrotl(x,y) +#else + #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (-(y) & 31))) +#endif + +#if defined(STBI_MALLOC) && defined(STBI_FREE) && (defined(STBI_REALLOC) || defined(STBI_REALLOC_SIZED)) +// ok +#elif !defined(STBI_MALLOC) && !defined(STBI_FREE) && !defined(STBI_REALLOC) && !defined(STBI_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBI_MALLOC, STBI_FREE, and STBI_REALLOC (or STBI_REALLOC_SIZED)." +#endif + +#ifndef STBI_MALLOC +#define STBI_MALLOC(sz) malloc(sz) +#define STBI_REALLOC(p,newsz) realloc(p,newsz) +#define STBI_FREE(p) free(p) +#endif + +#ifndef STBI_REALLOC_SIZED +#define STBI_REALLOC_SIZED(p,oldsz,newsz) STBI_REALLOC(p,newsz) +#endif + +// x86/x64 detection +#if defined(__x86_64__) || defined(_M_X64) +#define STBI__X64_TARGET +#elif defined(__i386) || defined(_M_IX86) +#define STBI__X86_TARGET +#endif + +#if defined(__GNUC__) && defined(STBI__X86_TARGET) && !defined(__SSE2__) && !defined(STBI_NO_SIMD) +// gcc doesn't support sse2 intrinsics unless you compile with -msse2, +// which in turn means it gets to use SSE2 everywhere. This is unfortunate, +// but previous attempts to provide the SSE2 functions with runtime +// detection caused numerous issues. The way architecture extensions are +// exposed in GCC/Clang is, sadly, not really suited for one-file libs. +// New behavior: if compiled with -msse2, we use SSE2 without any +// detection; if not, we don't use it at all. +#define STBI_NO_SIMD +#endif + +#if defined(__MINGW32__) && defined(STBI__X86_TARGET) && !defined(STBI_MINGW_ENABLE_SSE2) && !defined(STBI_NO_SIMD) +// Note that __MINGW32__ doesn't actually mean 32-bit, so we have to avoid STBI__X64_TARGET +// +// 32-bit MinGW wants ESP to be 16-byte aligned, but this is not in the +// Windows ABI and VC++ as well as Windows DLLs don't maintain that invariant. +// As a result, enabling SSE2 on 32-bit MinGW is dangerous when not +// simultaneously enabling "-mstackrealign". +// +// See https://github.com/nothings/stb/issues/81 for more information. +// +// So default to no SSE2 on 32-bit MinGW. If you've read this far and added +// -mstackrealign to your build settings, feel free to #define STBI_MINGW_ENABLE_SSE2. +#define STBI_NO_SIMD +#endif + +#if !defined(STBI_NO_SIMD) && (defined(STBI__X86_TARGET) || defined(STBI__X64_TARGET)) +#define STBI_SSE2 +#include + +#ifdef _MSC_VER + +#if _MSC_VER >= 1400 // not VC6 +#include // __cpuid +static int stbi__cpuid3(void) +{ + int info[4]; + __cpuid(info,1); + return info[3]; +} +#else +static int stbi__cpuid3(void) +{ + int res; + __asm { + mov eax,1 + cpuid + mov res,edx + } + return res; +} +#endif + +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + int info3 = stbi__cpuid3(); + return ((info3 >> 26) & 1) != 0; +} +#endif + +#else // assume GCC-style if not VC++ +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) + +#if !defined(STBI_NO_JPEG) && defined(STBI_SSE2) +static int stbi__sse2_available(void) +{ + // If we're even attempting to compile this on GCC/Clang, that means + // -msse2 is on, which means the compiler is allowed to use SSE2 + // instructions at will, and so are we. + return 1; +} +#endif + +#endif +#endif + +// ARM NEON +#if defined(STBI_NO_SIMD) && defined(STBI_NEON) +#undef STBI_NEON +#endif + +#ifdef STBI_NEON +#include +#ifdef _MSC_VER +#define STBI_SIMD_ALIGN(type, name) __declspec(align(16)) type name +#else +#define STBI_SIMD_ALIGN(type, name) type name __attribute__((aligned(16))) +#endif +#endif + +#ifndef STBI_SIMD_ALIGN +#define STBI_SIMD_ALIGN(type, name) type name +#endif + +#ifndef STBI_MAX_DIMENSIONS +#define STBI_MAX_DIMENSIONS (1 << 24) +#endif + +/////////////////////////////////////////////// +// +// stbi__context struct and start_xxx functions + +// stbi__context structure is our basic context used by all images, so it +// contains all the IO context, plus some basic image information +typedef struct +{ + stbi__uint32 img_x, img_y; + int img_n, img_out_n; + + stbi_io_callbacks io; + void *io_user_data; + + int read_from_callbacks; + int buflen; + stbi_uc buffer_start[128]; + int callback_already_read; + + stbi_uc *img_buffer, *img_buffer_end; + stbi_uc *img_buffer_original, *img_buffer_original_end; +} stbi__context; + + +static void stbi__refill_buffer(stbi__context *s); + +// initialize a memory-decode context +static void stbi__start_mem(stbi__context *s, stbi_uc const *buffer, int len) +{ + s->io.read = NULL; + s->read_from_callbacks = 0; + s->callback_already_read = 0; + s->img_buffer = s->img_buffer_original = (stbi_uc *) buffer; + s->img_buffer_end = s->img_buffer_original_end = (stbi_uc *) buffer+len; +} + +// initialize a callback-based context +static void stbi__start_callbacks(stbi__context *s, stbi_io_callbacks *c, void *user) +{ + s->io = *c; + s->io_user_data = user; + s->buflen = sizeof(s->buffer_start); + s->read_from_callbacks = 1; + s->callback_already_read = 0; + s->img_buffer = s->img_buffer_original = s->buffer_start; + stbi__refill_buffer(s); + s->img_buffer_original_end = s->img_buffer_end; +} + +#ifndef STBI_NO_STDIO + +static int stbi__stdio_read(void *user, char *data, int size) +{ + return (int) fread(data,1,size,(FILE*) user); +} + +static void stbi__stdio_skip(void *user, int n) +{ + int ch; + fseek((FILE*) user, n, SEEK_CUR); + ch = fgetc((FILE*) user); /* have to read a byte to reset feof()'s flag */ + if (ch != EOF) { + ungetc(ch, (FILE *) user); /* push byte back onto stream if valid. */ + } +} + +static int stbi__stdio_eof(void *user) +{ + return feof((FILE*) user) || ferror((FILE *) user); +} + +static stbi_io_callbacks stbi__stdio_callbacks = +{ + stbi__stdio_read, + stbi__stdio_skip, + stbi__stdio_eof, +}; + +static void stbi__start_file(stbi__context *s, FILE *f) +{ + stbi__start_callbacks(s, &stbi__stdio_callbacks, (void *) f); +} + +//static void stop_file(stbi__context *s) { } + +#endif // !STBI_NO_STDIO + +static void stbi__rewind(stbi__context *s) +{ + // conceptually rewind SHOULD rewind to the beginning of the stream, + // but we just rewind to the beginning of the initial buffer, because + // we only use it after doing 'test', which only ever looks at at most 92 bytes + s->img_buffer = s->img_buffer_original; + s->img_buffer_end = s->img_buffer_original_end; +} + +enum +{ + STBI_ORDER_RGB, + STBI_ORDER_BGR +}; + +typedef struct +{ + int bits_per_channel; + int num_channels; + int channel_order; +} stbi__result_info; + +#ifndef STBI_NO_JPEG +static int stbi__jpeg_test(stbi__context *s); +static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PNG +static int stbi__png_test(stbi__context *s); +static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__png_is16(stbi__context *s); +#endif + +#ifndef STBI_NO_BMP +static int stbi__bmp_test(stbi__context *s); +static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_TGA +static int stbi__tga_test(stbi__context *s); +static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context *s); +static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc); +static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__psd_is16(stbi__context *s); +#endif + +#ifndef STBI_NO_HDR +static int stbi__hdr_test(stbi__context *s); +static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_test(stbi__context *s); +static void *stbi__pic_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_GIF +static int stbi__gif_test(stbi__context *s); +static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp); +static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp); +#endif + +#ifndef STBI_NO_PNM +static int stbi__pnm_test(stbi__context *s); +static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp); +static int stbi__pnm_is16(stbi__context *s); +#endif + +static +#ifdef STBI_THREAD_LOCAL +STBI_THREAD_LOCAL +#endif +const char *stbi__g_failure_reason; + +STBIDEF const char *stbi_failure_reason(void) +{ + return stbi__g_failure_reason; +} + +#ifndef STBI_NO_FAILURE_STRINGS +static int stbi__err(const char *str) +{ + stbi__g_failure_reason = str; + return 0; +} +#endif + +static void *stbi__malloc(size_t size) +{ + return STBI_MALLOC(size); +} + +// stb_image uses ints pervasively, including for offset calculations. +// therefore the largest decoded image size we can support with the +// current code, even on 64-bit targets, is INT_MAX. this is not a +// significant limitation for the intended use case. +// +// we do, however, need to make sure our size calculations don't +// overflow. hence a few helper functions for size calculations that +// multiply integers together, making sure that they're non-negative +// and no overflow occurs. + +// return 1 if the sum is valid, 0 on overflow. +// negative terms are considered invalid. +static int stbi__addsizes_valid(int a, int b) +{ + if (b < 0) return 0; + // now 0 <= b <= INT_MAX, hence also + // 0 <= INT_MAX - b <= INTMAX. + // And "a + b <= INT_MAX" (which might overflow) is the + // same as a <= INT_MAX - b (no overflow) + return a <= INT_MAX - b; +} + +// returns 1 if the product is valid, 0 on overflow. +// negative factors are considered invalid. +static int stbi__mul2sizes_valid(int a, int b) +{ + if (a < 0 || b < 0) return 0; + if (b == 0) return 1; // mul-by-0 is always safe + // portable way to check for no overflows in a*b + return a <= INT_MAX/b; +} + +#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) +// returns 1 if "a*b + add" has no negative terms/factors and doesn't overflow +static int stbi__mad2sizes_valid(int a, int b, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__addsizes_valid(a*b, add); +} +#endif + +// returns 1 if "a*b*c + add" has no negative terms/factors and doesn't overflow +static int stbi__mad3sizes_valid(int a, int b, int c, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && + stbi__addsizes_valid(a*b*c, add); +} + +// returns 1 if "a*b*c*d + add" has no negative terms/factors and doesn't overflow +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) +static int stbi__mad4sizes_valid(int a, int b, int c, int d, int add) +{ + return stbi__mul2sizes_valid(a, b) && stbi__mul2sizes_valid(a*b, c) && + stbi__mul2sizes_valid(a*b*c, d) && stbi__addsizes_valid(a*b*c*d, add); +} +#endif + +#if !defined(STBI_NO_JPEG) || !defined(STBI_NO_PNG) || !defined(STBI_NO_TGA) || !defined(STBI_NO_HDR) +// mallocs with size overflow checking +static void *stbi__malloc_mad2(int a, int b, int add) +{ + if (!stbi__mad2sizes_valid(a, b, add)) return NULL; + return stbi__malloc(a*b + add); +} +#endif + +static void *stbi__malloc_mad3(int a, int b, int c, int add) +{ + if (!stbi__mad3sizes_valid(a, b, c, add)) return NULL; + return stbi__malloc(a*b*c + add); +} + +#if !defined(STBI_NO_LINEAR) || !defined(STBI_NO_HDR) || !defined(STBI_NO_PNM) +static void *stbi__malloc_mad4(int a, int b, int c, int d, int add) +{ + if (!stbi__mad4sizes_valid(a, b, c, d, add)) return NULL; + return stbi__malloc(a*b*c*d + add); +} +#endif + +// returns 1 if the sum of two signed ints is valid (between -2^31 and 2^31-1 inclusive), 0 on overflow. +static int stbi__addints_valid(int a, int b) +{ + if ((a >= 0) != (b >= 0)) return 1; // a and b have different signs, so no overflow + if (a < 0 && b < 0) return a >= INT_MIN - b; // same as a + b >= INT_MIN; INT_MIN - b cannot overflow since b < 0. + return a <= INT_MAX - b; +} + +// returns 1 if the product of two ints fits in a signed short, 0 on overflow. +static int stbi__mul2shorts_valid(int a, int b) +{ + if (b == 0 || b == -1) return 1; // multiplication by 0 is always 0; check for -1 so SHRT_MIN/b doesn't overflow + if ((a >= 0) == (b >= 0)) return a <= SHRT_MAX/b; // product is positive, so similar to mul2sizes_valid + if (b < 0) return a <= SHRT_MIN / b; // same as a * b >= SHRT_MIN + return a >= SHRT_MIN / b; +} + +// stbi__err - error +// stbi__errpf - error returning pointer to float +// stbi__errpuc - error returning pointer to unsigned char + +#ifdef STBI_NO_FAILURE_STRINGS + #define stbi__err(x,y) 0 +#elif defined(STBI_FAILURE_USERMSG) + #define stbi__err(x,y) stbi__err(y) +#else + #define stbi__err(x,y) stbi__err(x) +#endif + +#define stbi__errpf(x,y) ((float *)(size_t) (stbi__err(x,y)?NULL:NULL)) +#define stbi__errpuc(x,y) ((unsigned char *)(size_t) (stbi__err(x,y)?NULL:NULL)) + +STBIDEF void stbi_image_free(void *retval_from_stbi_load) +{ + STBI_FREE(retval_from_stbi_load); +} + +#ifndef STBI_NO_LINEAR +static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp); +#endif + +#ifndef STBI_NO_HDR +static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp); +#endif + +static int stbi__vertically_flip_on_load_global = 0; + +STBIDEF void stbi_set_flip_vertically_on_load(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load_global = flag_true_if_should_flip; +} + +#ifndef STBI_THREAD_LOCAL +#define stbi__vertically_flip_on_load stbi__vertically_flip_on_load_global +#else +static STBI_THREAD_LOCAL int stbi__vertically_flip_on_load_local, stbi__vertically_flip_on_load_set; + +STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_flip) +{ + stbi__vertically_flip_on_load_local = flag_true_if_should_flip; + stbi__vertically_flip_on_load_set = 1; +} + +#define stbi__vertically_flip_on_load (stbi__vertically_flip_on_load_set \ + ? stbi__vertically_flip_on_load_local \ + : stbi__vertically_flip_on_load_global) +#endif // STBI_THREAD_LOCAL + +static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +{ + memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields + ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed + ri->channel_order = STBI_ORDER_RGB; // all current input & output are this, but this is here so we can add BGR order + ri->num_channels = 0; + + // test the formats with a very explicit header first (at least a FOURCC + // or distinctive magic number first) + #ifndef STBI_NO_PNG + if (stbi__png_test(s)) return stbi__png_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_BMP + if (stbi__bmp_test(s)) return stbi__bmp_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_GIF + if (stbi__gif_test(s)) return stbi__gif_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_PSD + if (stbi__psd_test(s)) return stbi__psd_load(s,x,y,comp,req_comp, ri, bpc); + #else + STBI_NOTUSED(bpc); + #endif + #ifndef STBI_NO_PIC + if (stbi__pic_test(s)) return stbi__pic_load(s,x,y,comp,req_comp, ri); + #endif + + // then the formats that can end up attempting to load with just 1 or 2 + // bytes matching expectations; these are prone to false positives, so + // try them later + #ifndef STBI_NO_JPEG + if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); + #endif + #ifndef STBI_NO_PNM + if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri); + #endif + + #ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + float *hdr = stbi__hdr_load(s, x,y,comp,req_comp, ri); + return stbi__hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp); + } + #endif + + #ifndef STBI_NO_TGA + // test tga last because it's a crappy test! + if (stbi__tga_test(s)) + return stbi__tga_load(s,x,y,comp,req_comp, ri); + #endif + + return stbi__errpuc("unknown image type", "Image not of any known type, or corrupt"); +} + +static stbi_uc *stbi__convert_16_to_8(stbi__uint16 *orig, int w, int h, int channels) +{ + int i; + int img_len = w * h * channels; + stbi_uc *reduced; + + reduced = (stbi_uc *) stbi__malloc(img_len); + if (reduced == NULL) return stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + reduced[i] = (stbi_uc)((orig[i] >> 8) & 0xFF); // top half of each byte is sufficient approx of 16->8 bit scaling + + STBI_FREE(orig); + return reduced; +} + +static stbi__uint16 *stbi__convert_8_to_16(stbi_uc *orig, int w, int h, int channels) +{ + int i; + int img_len = w * h * channels; + stbi__uint16 *enlarged; + + enlarged = (stbi__uint16 *) stbi__malloc(img_len*2); + if (enlarged == NULL) return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); + + for (i = 0; i < img_len; ++i) + enlarged[i] = (stbi__uint16)((orig[i] << 8) + orig[i]); // replicate to high and low byte, maps 0->0, 255->0xffff + + STBI_FREE(orig); + return enlarged; +} + +static void stbi__vertical_flip(void *image, int w, int h, int bytes_per_pixel) +{ + int row; + size_t bytes_per_row = (size_t)w * bytes_per_pixel; + stbi_uc temp[2048]; + stbi_uc *bytes = (stbi_uc *)image; + + for (row = 0; row < (h>>1); row++) { + stbi_uc *row0 = bytes + row*bytes_per_row; + stbi_uc *row1 = bytes + (h - row - 1)*bytes_per_row; + // swap row0 with row1 + size_t bytes_left = bytes_per_row; + while (bytes_left) { + size_t bytes_copy = (bytes_left < sizeof(temp)) ? bytes_left : sizeof(temp); + memcpy(temp, row0, bytes_copy); + memcpy(row0, row1, bytes_copy); + memcpy(row1, temp, bytes_copy); + row0 += bytes_copy; + row1 += bytes_copy; + bytes_left -= bytes_copy; + } + } +} + +#ifndef STBI_NO_GIF +static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int bytes_per_pixel) +{ + int slice; + int slice_size = w * h * bytes_per_pixel; + + stbi_uc *bytes = (stbi_uc *)image; + for (slice = 0; slice < z; ++slice) { + stbi__vertical_flip(bytes, w, h, bytes_per_pixel); + bytes += slice_size; + } +} +#endif + +static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + stbi__result_info ri; + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); + + if (result == NULL) + return NULL; + + // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. + STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); + + if (ri.bits_per_channel != 8) { + result = stbi__convert_16_to_8((stbi__uint16 *) result, *x, *y, req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 8; + } + + // @TODO: move stbi__convert_format to here + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi_uc)); + } + + return (unsigned char *) result; +} + +static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + stbi__result_info ri; + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); + + if (result == NULL) + return NULL; + + // it is the responsibility of the loaders to make sure we get either 8 or 16 bit. + STBI_ASSERT(ri.bits_per_channel == 8 || ri.bits_per_channel == 16); + + if (ri.bits_per_channel != 16) { + result = stbi__convert_8_to_16((stbi_uc *) result, *x, *y, req_comp == 0 ? *comp : req_comp); + ri.bits_per_channel = 16; + } + + // @TODO: move stbi__convert_format16 to here + // @TODO: special case RGB-to-Y (and RGBA-to-YA) for 8-bit-to-16-bit case to keep more precision + + if (stbi__vertically_flip_on_load) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(stbi__uint16)); + } + + return (stbi__uint16 *) result; +} + +#if !defined(STBI_NO_HDR) && !defined(STBI_NO_LINEAR) +static void stbi__float_postprocess(float *result, int *x, int *y, int *comp, int req_comp) +{ + if (stbi__vertically_flip_on_load && result != NULL) { + int channels = req_comp ? req_comp : *comp; + stbi__vertical_flip(result, *x, *y, channels * sizeof(float)); + } +} +#endif + +#ifndef STBI_NO_STDIO + +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) +STBI_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBI_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); +#endif + +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) +STBIDEF int stbi_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbi__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBI_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + + +STBIDEF stbi_uc *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + unsigned char *result; + if (!f) return stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +STBIDEF stbi_uc *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *result; + stbi__context s; + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi__uint16 *stbi_load_from_file_16(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi__uint16 *result; + stbi__context s; + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_16bit(&s,x,y,comp,req_comp); + if (result) { + // need to 'unget' all the characters in the IO buffer + fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + } + return result; +} + +STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + stbi__uint16 *result; + if (!f) return (stbi_us *) stbi__errpuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file_16(f,x,y,comp,req_comp); + fclose(f); + return result; +} + + +#endif //!STBI_NO_STDIO + +STBIDEF stbi_us *stbi_load_16_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); +} + +STBIDEF stbi_us *stbi_load_16_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *channels_in_file, int desired_channels) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *)clbk, user); + return stbi__load_and_postprocess_16bit(&s,x,y,channels_in_file,desired_channels); +} + +STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); +} + +STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_GIF +STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp) +{ + unsigned char *result; + stbi__context s; + stbi__start_mem(&s,buffer,len); + + result = (unsigned char*) stbi__load_gif_main(&s, delays, x, y, z, comp, req_comp); + if (stbi__vertically_flip_on_load) { + stbi__vertical_flip_slices( result, *x, *y, *z, *comp ); + } + + return result; +} +#endif + +#ifndef STBI_NO_LINEAR +static float *stbi__loadf_main(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *data; + #ifndef STBI_NO_HDR + if (stbi__hdr_test(s)) { + stbi__result_info ri; + float *hdr_data = stbi__hdr_load(s,x,y,comp,req_comp, &ri); + if (hdr_data) + stbi__float_postprocess(hdr_data,x,y,comp,req_comp); + return hdr_data; + } + #endif + data = stbi__load_and_postprocess_8bit(s, x, y, comp, req_comp); + if (data) + return stbi__ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); + return stbi__errpf("unknown image type", "Image not of any known type, or corrupt"); +} + +STBIDEF float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} + +STBIDEF float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_STDIO +STBIDEF float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + float *result; + FILE *f = stbi__fopen(filename, "rb"); + if (!f) return stbi__errpf("can't fopen", "Unable to open file"); + result = stbi_loadf_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +STBIDEF float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi__context s; + stbi__start_file(&s,f); + return stbi__loadf_main(&s,x,y,comp,req_comp); +} +#endif // !STBI_NO_STDIO + +#endif // !STBI_NO_LINEAR + +// these is-hdr-or-not is defined independent of whether STBI_NO_LINEAR is +// defined, for API simplicity; if STBI_NO_LINEAR is defined, it always +// reports false! + +STBIDEF int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len) +{ + #ifndef STBI_NO_HDR + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__hdr_test(&s); + #else + STBI_NOTUSED(buffer); + STBI_NOTUSED(len); + return 0; + #endif +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_is_hdr (char const *filename) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result=0; + if (f) { + result = stbi_is_hdr_from_file(f); + fclose(f); + } + return result; +} + +STBIDEF int stbi_is_hdr_from_file(FILE *f) +{ + #ifndef STBI_NO_HDR + long pos = ftell(f); + int res; + stbi__context s; + stbi__start_file(&s,f); + res = stbi__hdr_test(&s); + fseek(f, pos, SEEK_SET); + return res; + #else + STBI_NOTUSED(f); + return 0; + #endif +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user) +{ + #ifndef STBI_NO_HDR + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi__hdr_test(&s); + #else + STBI_NOTUSED(clbk); + STBI_NOTUSED(user); + return 0; + #endif +} + +#ifndef STBI_NO_LINEAR +static float stbi__l2h_gamma=2.2f, stbi__l2h_scale=1.0f; + +STBIDEF void stbi_ldr_to_hdr_gamma(float gamma) { stbi__l2h_gamma = gamma; } +STBIDEF void stbi_ldr_to_hdr_scale(float scale) { stbi__l2h_scale = scale; } +#endif + +static float stbi__h2l_gamma_i=1.0f/2.2f, stbi__h2l_scale_i=1.0f; + +STBIDEF void stbi_hdr_to_ldr_gamma(float gamma) { stbi__h2l_gamma_i = 1/gamma; } +STBIDEF void stbi_hdr_to_ldr_scale(float scale) { stbi__h2l_scale_i = 1/scale; } + + +////////////////////////////////////////////////////////////////////////////// +// +// Common code used by all image loaders +// + +enum +{ + STBI__SCAN_load=0, + STBI__SCAN_type, + STBI__SCAN_header +}; + +static void stbi__refill_buffer(stbi__context *s) +{ + int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); + s->callback_already_read += (int) (s->img_buffer - s->img_buffer_original); + if (n == 0) { + // at end of file, treat same as if from memory, but need to handle case + // where s->img_buffer isn't pointing to safe memory, e.g. 0-byte file + s->read_from_callbacks = 0; + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start+1; + *s->img_buffer = 0; + } else { + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start + n; + } +} + +stbi_inline static stbi_uc stbi__get8(stbi__context *s) +{ + if (s->img_buffer < s->img_buffer_end) + return *s->img_buffer++; + if (s->read_from_callbacks) { + stbi__refill_buffer(s); + return *s->img_buffer++; + } + return 0; +} + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_HDR) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +stbi_inline static int stbi__at_eof(stbi__context *s) +{ + if (s->io.read) { + if (!(s->io.eof)(s->io_user_data)) return 0; + // if feof() is true, check if buffer = end + // special case: we've only got the special 0 character at the end + if (s->read_from_callbacks == 0) return 1; + } + + return s->img_buffer >= s->img_buffer_end; +} +#endif + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) +// nothing +#else +static void stbi__skip(stbi__context *s, int n) +{ + if (n == 0) return; // already there! + if (n < 0) { + s->img_buffer = s->img_buffer_end; + return; + } + if (s->io.read) { + int blen = (int) (s->img_buffer_end - s->img_buffer); + if (blen < n) { + s->img_buffer = s->img_buffer_end; + (s->io.skip)(s->io_user_data, n - blen); + return; + } + } + s->img_buffer += n; +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_TGA) && defined(STBI_NO_HDR) && defined(STBI_NO_PNM) +// nothing +#else +static int stbi__getn(stbi__context *s, stbi_uc *buffer, int n) +{ + if (s->io.read) { + int blen = (int) (s->img_buffer_end - s->img_buffer); + if (blen < n) { + int res, count; + + memcpy(buffer, s->img_buffer, blen); + + count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen); + res = (count == (n-blen)); + s->img_buffer = s->img_buffer_end; + return res; + } + } + + if (s->img_buffer+n <= s->img_buffer_end) { + memcpy(buffer, s->img_buffer, n); + s->img_buffer += n; + return 1; + } else + return 0; +} +#endif + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) +// nothing +#else +static int stbi__get16be(stbi__context *s) +{ + int z = stbi__get8(s); + return (z << 8) + stbi__get8(s); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) && defined(STBI_NO_PIC) +// nothing +#else +static stbi__uint32 stbi__get32be(stbi__context *s) +{ + stbi__uint32 z = stbi__get16be(s); + return (z << 16) + stbi__get16be(s); +} +#endif + +#if defined(STBI_NO_BMP) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) +// nothing +#else +static int stbi__get16le(stbi__context *s) +{ + int z = stbi__get8(s); + return z + (stbi__get8(s) << 8); +} +#endif + +#ifndef STBI_NO_BMP +static stbi__uint32 stbi__get32le(stbi__context *s) +{ + stbi__uint32 z = stbi__get16le(s); + z += (stbi__uint32)stbi__get16le(s) << 16; + return z; +} +#endif + +#define STBI__BYTECAST(x) ((stbi_uc) ((x) & 255)) // truncate int to byte without warnings + +#if defined(STBI_NO_JPEG) && defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +////////////////////////////////////////////////////////////////////////////// +// +// generic converter from built-in img_n to req_comp +// individual types do this automatically as much as possible (e.g. jpeg +// does all cases internally since it needs to colorspace convert anyway, +// and it never has alpha, so very few cases ). png can automatically +// interleave an alpha=255 channel, but falls back to this for other cases +// +// assume data buffer is malloced, so malloc a new one and free that one +// only failure mode is malloc failing + +static stbi_uc stbi__compute_y(int r, int g, int b) +{ + return (stbi_uc) (((r*77) + (g*150) + (29*b)) >> 8); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_BMP) && defined(STBI_NO_PSD) && defined(STBI_NO_TGA) && defined(STBI_NO_GIF) && defined(STBI_NO_PIC) && defined(STBI_NO_PNM) +// nothing +#else +static unsigned char *stbi__convert_format(unsigned char *data, int img_n, int req_comp, unsigned int x, unsigned int y) +{ + int i,j; + unsigned char *good; + + if (req_comp == img_n) return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (unsigned char *) stbi__malloc_mad3(req_comp, x, y, 0); + if (good == NULL) { + STBI_FREE(data); + return stbi__errpuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + unsigned char *src = data + j * x * img_n ; + unsigned char *dest = good + j * x * req_comp; + + #define STBI__COMBO(a,b) ((a)*8+(b)) + #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=255; } break; + STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=255; } break; + STBI__CASE(2,1) { dest[0]=src[0]; } break; + STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; + STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=255; } break; + STBI__CASE(3,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = 255; } break; + STBI__CASE(4,1) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=stbi__compute_y(src[0],src[1],src[2]); dest[1] = src[3]; } break; + STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; + default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return stbi__errpuc("unsupported", "Unsupported format conversion"); + } + #undef STBI__CASE + } + + STBI_FREE(data); + return good; +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) +// nothing +#else +static stbi__uint16 stbi__compute_y_16(int r, int g, int b) +{ + return (stbi__uint16) (((r*77) + (g*150) + (29*b)) >> 8); +} +#endif + +#if defined(STBI_NO_PNG) && defined(STBI_NO_PSD) +// nothing +#else +static stbi__uint16 *stbi__convert_format16(stbi__uint16 *data, int img_n, int req_comp, unsigned int x, unsigned int y) +{ + int i,j; + stbi__uint16 *good; + + if (req_comp == img_n) return data; + STBI_ASSERT(req_comp >= 1 && req_comp <= 4); + + good = (stbi__uint16 *) stbi__malloc(req_comp * x * y * 2); + if (good == NULL) { + STBI_FREE(data); + return (stbi__uint16 *) stbi__errpuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + stbi__uint16 *src = data + j * x * img_n ; + stbi__uint16 *dest = good + j * x * req_comp; + + #define STBI__COMBO(a,b) ((a)*8+(b)) + #define STBI__CASE(a,b) case STBI__COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (STBI__COMBO(img_n, req_comp)) { + STBI__CASE(1,2) { dest[0]=src[0]; dest[1]=0xffff; } break; + STBI__CASE(1,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(1,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=0xffff; } break; + STBI__CASE(2,1) { dest[0]=src[0]; } break; + STBI__CASE(2,3) { dest[0]=dest[1]=dest[2]=src[0]; } break; + STBI__CASE(2,4) { dest[0]=dest[1]=dest[2]=src[0]; dest[3]=src[1]; } break; + STBI__CASE(3,4) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2];dest[3]=0xffff; } break; + STBI__CASE(3,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(3,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = 0xffff; } break; + STBI__CASE(4,1) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); } break; + STBI__CASE(4,2) { dest[0]=stbi__compute_y_16(src[0],src[1],src[2]); dest[1] = src[3]; } break; + STBI__CASE(4,3) { dest[0]=src[0];dest[1]=src[1];dest[2]=src[2]; } break; + default: STBI_ASSERT(0); STBI_FREE(data); STBI_FREE(good); return (stbi__uint16*) stbi__errpuc("unsupported", "Unsupported format conversion"); + } + #undef STBI__CASE + } + + STBI_FREE(data); + return good; +} +#endif + +#ifndef STBI_NO_LINEAR +static float *stbi__ldr_to_hdr(stbi_uc *data, int x, int y, int comp) +{ + int i,k,n; + float *output; + if (!data) return NULL; + output = (float *) stbi__malloc_mad4(x, y, comp, sizeof(float), 0); + if (output == NULL) { STBI_FREE(data); return stbi__errpf("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + output[i*comp + k] = (float) (pow(data[i*comp+k]/255.0f, stbi__l2h_gamma) * stbi__l2h_scale); + } + } + if (n < comp) { + for (i=0; i < x*y; ++i) { + output[i*comp + n] = data[i*comp + n]/255.0f; + } + } + STBI_FREE(data); + return output; +} +#endif + +#ifndef STBI_NO_HDR +#define stbi__float2int(x) ((int) (x)) +static stbi_uc *stbi__hdr_to_ldr(float *data, int x, int y, int comp) +{ + int i,k,n; + stbi_uc *output; + if (!data) return NULL; + output = (stbi_uc *) stbi__malloc_mad3(x, y, comp, 0); + if (output == NULL) { STBI_FREE(data); return stbi__errpuc("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + float z = (float) pow(data[i*comp+k]*stbi__h2l_scale_i, stbi__h2l_gamma_i) * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (stbi_uc) stbi__float2int(z); + } + if (k < comp) { + float z = data[i*comp+k] * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (stbi_uc) stbi__float2int(z); + } + } + STBI_FREE(data); + return output; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// "baseline" JPEG/JFIF decoder +// +// simple implementation +// - doesn't support delayed output of y-dimension +// - simple interface (only one output format: 8-bit interleaved RGB) +// - doesn't try to recover corrupt jpegs +// - doesn't allow partial loading, loading multiple at once +// - still fast on x86 (copying globals into locals doesn't help x86) +// - allocates lots of intermediate memory (full size of all components) +// - non-interleaved case requires this anyway +// - allows good upsampling (see next) +// high-quality +// - upsampled channels are bilinearly interpolated, even across blocks +// - quality integer IDCT derived from IJG's 'slow' +// performance +// - fast huffman; reasonable integer IDCT +// - some SIMD kernels for common paths on targets with SSE2/NEON +// - uses a lot of intermediate memory, could cache poorly + +#ifndef STBI_NO_JPEG + +// huffman decoding acceleration +#define FAST_BITS 9 // larger handles more cases; smaller stomps less cache + +typedef struct +{ + stbi_uc fast[1 << FAST_BITS]; + // weirdly, repacking this into AoS is a 10% speed loss, instead of a win + stbi__uint16 code[256]; + stbi_uc values[256]; + stbi_uc size[257]; + unsigned int maxcode[18]; + int delta[17]; // old 'firstsymbol' - old 'firstcode' +} stbi__huffman; + +typedef struct +{ + stbi__context *s; + stbi__huffman huff_dc[4]; + stbi__huffman huff_ac[4]; + stbi__uint16 dequant[4][64]; + stbi__int16 fast_ac[4][1 << FAST_BITS]; + +// sizes for components, interleaved MCUs + int img_h_max, img_v_max; + int img_mcu_x, img_mcu_y; + int img_mcu_w, img_mcu_h; + +// definition of jpeg image component + struct + { + int id; + int h,v; + int tq; + int hd,ha; + int dc_pred; + + int x,y,w2,h2; + stbi_uc *data; + void *raw_data, *raw_coeff; + stbi_uc *linebuf; + short *coeff; // progressive only + int coeff_w, coeff_h; // number of 8x8 coefficient blocks + } img_comp[4]; + + stbi__uint32 code_buffer; // jpeg entropy-coded buffer + int code_bits; // number of valid bits + unsigned char marker; // marker seen while filling entropy buffer + int nomore; // flag if we saw a marker so must stop + + int progressive; + int spec_start; + int spec_end; + int succ_high; + int succ_low; + int eob_run; + int jfif; + int app14_color_transform; // Adobe APP14 tag + int rgb; + + int scan_n, order[4]; + int restart_interval, todo; + +// kernels + void (*idct_block_kernel)(stbi_uc *out, int out_stride, short data[64]); + void (*YCbCr_to_RGB_kernel)(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step); + stbi_uc *(*resample_row_hv_2_kernel)(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs); +} stbi__jpeg; + +static int stbi__build_huffman(stbi__huffman *h, int *count) +{ + int i,j,k=0; + unsigned int code; + // build size list for each symbol (from JPEG spec) + for (i=0; i < 16; ++i) { + for (j=0; j < count[i]; ++j) { + h->size[k++] = (stbi_uc) (i+1); + if(k >= 257) return stbi__err("bad size list","Corrupt JPEG"); + } + } + h->size[k] = 0; + + // compute actual symbols (from jpeg spec) + code = 0; + k = 0; + for(j=1; j <= 16; ++j) { + // compute delta to add to code to compute symbol id + h->delta[j] = k - code; + if (h->size[k] == j) { + while (h->size[k] == j) + h->code[k++] = (stbi__uint16) (code++); + if (code-1 >= (1u << j)) return stbi__err("bad code lengths","Corrupt JPEG"); + } + // compute largest code + 1 for this size, preshifted as needed later + h->maxcode[j] = code << (16-j); + code <<= 1; + } + h->maxcode[j] = 0xffffffff; + + // build non-spec acceleration table; 255 is flag for not-accelerated + memset(h->fast, 255, 1 << FAST_BITS); + for (i=0; i < k; ++i) { + int s = h->size[i]; + if (s <= FAST_BITS) { + int c = h->code[i] << (FAST_BITS-s); + int m = 1 << (FAST_BITS-s); + for (j=0; j < m; ++j) { + h->fast[c+j] = (stbi_uc) i; + } + } + } + return 1; +} + +// build a table that decodes both magnitude and value of small ACs in +// one go. +static void stbi__build_fast_ac(stbi__int16 *fast_ac, stbi__huffman *h) +{ + int i; + for (i=0; i < (1 << FAST_BITS); ++i) { + stbi_uc fast = h->fast[i]; + fast_ac[i] = 0; + if (fast < 255) { + int rs = h->values[fast]; + int run = (rs >> 4) & 15; + int magbits = rs & 15; + int len = h->size[fast]; + + if (magbits && len + magbits <= FAST_BITS) { + // magnitude code followed by receive_extend code + int k = ((i << len) & ((1 << FAST_BITS) - 1)) >> (FAST_BITS - magbits); + int m = 1 << (magbits - 1); + if (k < m) k += (~0U << magbits) + 1; + // if the result is small enough, we can fit it in fast_ac table + if (k >= -128 && k <= 127) + fast_ac[i] = (stbi__int16) ((k * 256) + (run * 16) + (len + magbits)); + } + } + } +} + +static void stbi__grow_buffer_unsafe(stbi__jpeg *j) +{ + do { + unsigned int b = j->nomore ? 0 : stbi__get8(j->s); + if (b == 0xff) { + int c = stbi__get8(j->s); + while (c == 0xff) c = stbi__get8(j->s); // consume fill bytes + if (c != 0) { + j->marker = (unsigned char) c; + j->nomore = 1; + return; + } + } + j->code_buffer |= b << (24 - j->code_bits); + j->code_bits += 8; + } while (j->code_bits <= 24); +} + +// (1 << n) - 1 +static const stbi__uint32 stbi__bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535}; + +// decode a jpeg huffman value from the bitstream +stbi_inline static int stbi__jpeg_huff_decode(stbi__jpeg *j, stbi__huffman *h) +{ + unsigned int temp; + int c,k; + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + + // look at the top FAST_BITS and determine what symbol ID it is, + // if the code is <= FAST_BITS + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + k = h->fast[c]; + if (k < 255) { + int s = h->size[k]; + if (s > j->code_bits) + return -1; + j->code_buffer <<= s; + j->code_bits -= s; + return h->values[k]; + } + + // naive test is to shift the code_buffer down so k bits are + // valid, then test against maxcode. To speed this up, we've + // preshifted maxcode left so that it has (16-k) 0s at the + // end; in other words, regardless of the number of bits, it + // wants to be compared against something shifted to have 16; + // that way we don't need to shift inside the loop. + temp = j->code_buffer >> 16; + for (k=FAST_BITS+1 ; ; ++k) + if (temp < h->maxcode[k]) + break; + if (k == 17) { + // error! code not found + j->code_bits -= 16; + return -1; + } + + if (k > j->code_bits) + return -1; + + // convert the huffman code to the symbol id + c = ((j->code_buffer >> (32 - k)) & stbi__bmask[k]) + h->delta[k]; + if(c < 0 || c >= 256) // symbol id out of bounds! + return -1; + STBI_ASSERT((((j->code_buffer) >> (32 - h->size[c])) & stbi__bmask[h->size[c]]) == h->code[c]); + + // convert the id to a symbol + j->code_bits -= k; + j->code_buffer <<= k; + return h->values[c]; +} + +// bias[n] = (-1<code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing + + sgn = j->code_buffer >> 31; // sign bit always in MSB; 0 if MSB clear (positive), 1 if MSB set (negative) + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k + (stbi__jbias[n] & (sgn - 1)); +} + +// get some unsigned bits +stbi_inline static int stbi__jpeg_get_bits(stbi__jpeg *j, int n) +{ + unsigned int k; + if (j->code_bits < n) stbi__grow_buffer_unsafe(j); + if (j->code_bits < n) return 0; // ran out of bits from stream, return 0s intead of continuing + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~stbi__bmask[n]; + k &= stbi__bmask[n]; + j->code_bits -= n; + return k; +} + +stbi_inline static int stbi__jpeg_get_bit(stbi__jpeg *j) +{ + unsigned int k; + if (j->code_bits < 1) stbi__grow_buffer_unsafe(j); + if (j->code_bits < 1) return 0; // ran out of bits from stream, return 0s intead of continuing + k = j->code_buffer; + j->code_buffer <<= 1; + --j->code_bits; + return k & 0x80000000; +} + +// given a value that's at position X in the zigzag stream, +// where does it appear in the 8x8 matrix coded as row-major? +static const stbi_uc stbi__jpeg_dezigzag[64+15] = +{ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + // let corrupt input sample past end + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63 +}; + +// decode one 64-entry block-- +static int stbi__jpeg_decode_block(stbi__jpeg *j, short data[64], stbi__huffman *hdc, stbi__huffman *hac, stbi__int16 *fac, int b, stbi__uint16 *dequant) +{ + int diff,dc,k; + int t; + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0 || t > 15) return stbi__err("bad huffman code","Corrupt JPEG"); + + // 0 all the ac values now so we can do it 32-bits at a time + memset(data,0,64*sizeof(data[0])); + + diff = t ? stbi__extend_receive(j, t) : 0; + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta","Corrupt JPEG"); + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, dequant[0])) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * dequant[0]); + + // decode AC components, see JPEG spec + k = 1; + do { + unsigned int zig; + int c,r,s; + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); + j->code_buffer <<= s; + j->code_bits -= s; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) ((r >> 8) * dequant[zig]); + } else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (rs != 0xf0) break; // end block + k += 16; + } else { + k += r; + // decode into unzigzag'd location + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) (stbi__extend_receive(j,s) * dequant[zig]); + } + } + } while (k < 64); + return 1; +} + +static int stbi__jpeg_decode_block_prog_dc(stbi__jpeg *j, short data[64], stbi__huffman *hdc, int b) +{ + int diff,dc; + int t; + if (j->spec_end != 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + + if (j->succ_high == 0) { + // first scan for DC coefficient, must be first + memset(data,0,64*sizeof(data[0])); // 0 all the ac values now + t = stbi__jpeg_huff_decode(j, hdc); + if (t < 0 || t > 15) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + diff = t ? stbi__extend_receive(j, t) : 0; + + if (!stbi__addints_valid(j->img_comp[b].dc_pred, diff)) return stbi__err("bad delta", "Corrupt JPEG"); + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + if (!stbi__mul2shorts_valid(dc, 1 << j->succ_low)) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + data[0] = (short) (dc * (1 << j->succ_low)); + } else { + // refinement scan for DC coefficient + if (stbi__jpeg_get_bit(j)) + data[0] += (short) (1 << j->succ_low); + } + return 1; +} + +// @OPTIMIZE: store non-zigzagged during the decode passes, +// and only de-zigzag when dequantizing +static int stbi__jpeg_decode_block_prog_ac(stbi__jpeg *j, short data[64], stbi__huffman *hac, stbi__int16 *fac) +{ + int k; + if (j->spec_start == 0) return stbi__err("can't merge dc and ac", "Corrupt JPEG"); + + if (j->succ_high == 0) { + int shift = j->succ_low; + + if (j->eob_run) { + --j->eob_run; + return 1; + } + + k = j->spec_start; + do { + unsigned int zig; + int c,r,s; + if (j->code_bits < 16) stbi__grow_buffer_unsafe(j); + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + r = fac[c]; + if (r) { // fast-AC path + k += (r >> 4) & 15; // run + s = r & 15; // combined length + if (s > j->code_bits) return stbi__err("bad huffman code", "Combined length longer than code bits available"); + j->code_buffer <<= s; + j->code_bits -= s; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) ((r >> 8) * (1 << shift)); + } else { + int rs = stbi__jpeg_huff_decode(j, hac); + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r); + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + --j->eob_run; + break; + } + k += 16; + } else { + k += r; + zig = stbi__jpeg_dezigzag[k++]; + data[zig] = (short) (stbi__extend_receive(j,s) * (1 << shift)); + } + } + } while (k <= j->spec_end); + } else { + // refinement scan for these AC coefficients + + short bit = (short) (1 << j->succ_low); + + if (j->eob_run) { + --j->eob_run; + for (k = j->spec_start; k <= j->spec_end; ++k) { + short *p = &data[stbi__jpeg_dezigzag[k]]; + if (*p != 0) + if (stbi__jpeg_get_bit(j)) + if ((*p & bit)==0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } + } else { + k = j->spec_start; + do { + int r,s; + int rs = stbi__jpeg_huff_decode(j, hac); // @OPTIMIZE see if we can use the fast path here, advance-by-r is so slow, eh + if (rs < 0) return stbi__err("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (r < 15) { + j->eob_run = (1 << r) - 1; + if (r) + j->eob_run += stbi__jpeg_get_bits(j, r); + r = 64; // force end of block + } else { + // r=15 s=0 should write 16 0s, so we just do + // a run of 15 0s and then write s (which is 0), + // so we don't have to do anything special here + } + } else { + if (s != 1) return stbi__err("bad huffman code", "Corrupt JPEG"); + // sign bit + if (stbi__jpeg_get_bit(j)) + s = bit; + else + s = -bit; + } + + // advance by r + while (k <= j->spec_end) { + short *p = &data[stbi__jpeg_dezigzag[k++]]; + if (*p != 0) { + if (stbi__jpeg_get_bit(j)) + if ((*p & bit)==0) { + if (*p > 0) + *p += bit; + else + *p -= bit; + } + } else { + if (r == 0) { + *p = (short) s; + break; + } + --r; + } + } + } while (k <= j->spec_end); + } + } + return 1; +} + +// take a -128..127 value and stbi__clamp it and convert to 0..255 +stbi_inline static stbi_uc stbi__clamp(int x) +{ + // trick to use a single test to catch both cases + if ((unsigned int) x > 255) { + if (x < 0) return 0; + if (x > 255) return 255; + } + return (stbi_uc) x; +} + +#define stbi__f2f(x) ((int) (((x) * 4096 + 0.5))) +#define stbi__fsh(x) ((x) * 4096) + +// derived from jidctint -- DCT_ISLOW +#define STBI__IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \ + int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \ + p2 = s2; \ + p3 = s6; \ + p1 = (p2+p3) * stbi__f2f(0.5411961f); \ + t2 = p1 + p3*stbi__f2f(-1.847759065f); \ + t3 = p1 + p2*stbi__f2f( 0.765366865f); \ + p2 = s0; \ + p3 = s4; \ + t0 = stbi__fsh(p2+p3); \ + t1 = stbi__fsh(p2-p3); \ + x0 = t0+t3; \ + x3 = t0-t3; \ + x1 = t1+t2; \ + x2 = t1-t2; \ + t0 = s7; \ + t1 = s5; \ + t2 = s3; \ + t3 = s1; \ + p3 = t0+t2; \ + p4 = t1+t3; \ + p1 = t0+t3; \ + p2 = t1+t2; \ + p5 = (p3+p4)*stbi__f2f( 1.175875602f); \ + t0 = t0*stbi__f2f( 0.298631336f); \ + t1 = t1*stbi__f2f( 2.053119869f); \ + t2 = t2*stbi__f2f( 3.072711026f); \ + t3 = t3*stbi__f2f( 1.501321110f); \ + p1 = p5 + p1*stbi__f2f(-0.899976223f); \ + p2 = p5 + p2*stbi__f2f(-2.562915447f); \ + p3 = p3*stbi__f2f(-1.961570560f); \ + p4 = p4*stbi__f2f(-0.390180644f); \ + t3 += p1+p4; \ + t2 += p2+p3; \ + t1 += p2+p4; \ + t0 += p1+p3; + +static void stbi__idct_block(stbi_uc *out, int out_stride, short data[64]) +{ + int i,val[64],*v=val; + stbi_uc *o; + short *d = data; + + // columns + for (i=0; i < 8; ++i,++d, ++v) { + // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing + if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 + && d[40]==0 && d[48]==0 && d[56]==0) { + // no shortcut 0 seconds + // (1|2|3|4|5|6|7)==0 0 seconds + // all separate -0.047 seconds + // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds + int dcterm = d[0]*4; + v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; + } else { + STBI__IDCT_1D(d[ 0],d[ 8],d[16],d[24],d[32],d[40],d[48],d[56]) + // constants scaled things up by 1<<12; let's bring them back + // down, but keep 2 extra bits of precision + x0 += 512; x1 += 512; x2 += 512; x3 += 512; + v[ 0] = (x0+t3) >> 10; + v[56] = (x0-t3) >> 10; + v[ 8] = (x1+t2) >> 10; + v[48] = (x1-t2) >> 10; + v[16] = (x2+t1) >> 10; + v[40] = (x2-t1) >> 10; + v[24] = (x3+t0) >> 10; + v[32] = (x3-t0) >> 10; + } + } + + for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=out_stride) { + // no fast case since the first 1D IDCT spread components out + STBI__IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]) + // constants scaled things up by 1<<12, plus we had 1<<2 from first + // loop, plus horizontal and vertical each scale by sqrt(8) so together + // we've got an extra 1<<3, so 1<<17 total we need to remove. + // so we want to round that, which means adding 0.5 * 1<<17, + // aka 65536. Also, we'll end up with -128 to 127 that we want + // to encode as 0..255 by adding 128, so we'll add that before the shift + x0 += 65536 + (128<<17); + x1 += 65536 + (128<<17); + x2 += 65536 + (128<<17); + x3 += 65536 + (128<<17); + // tried computing the shifts into temps, or'ing the temps to see + // if any were out of range, but that was slower + o[0] = stbi__clamp((x0+t3) >> 17); + o[7] = stbi__clamp((x0-t3) >> 17); + o[1] = stbi__clamp((x1+t2) >> 17); + o[6] = stbi__clamp((x1-t2) >> 17); + o[2] = stbi__clamp((x2+t1) >> 17); + o[5] = stbi__clamp((x2-t1) >> 17); + o[3] = stbi__clamp((x3+t0) >> 17); + o[4] = stbi__clamp((x3-t0) >> 17); + } +} + +#ifdef STBI_SSE2 +// sse2 integer IDCT. not the fastest possible implementation but it +// produces bit-identical results to the generic C version so it's +// fully "transparent". +static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) +{ + // This is constructed to match our regular (generic) integer IDCT exactly. + __m128i row0, row1, row2, row3, row4, row5, row6, row7; + __m128i tmp; + + // dot product constant: even elems=x, odd elems=y + #define dct_const(x,y) _mm_setr_epi16((x),(y),(x),(y),(x),(y),(x),(y)) + + // out(0) = c0[even]*x + c0[odd]*y (c0, x, y 16-bit, out 32-bit) + // out(1) = c1[even]*x + c1[odd]*y + #define dct_rot(out0,out1, x,y,c0,c1) \ + __m128i c0##lo = _mm_unpacklo_epi16((x),(y)); \ + __m128i c0##hi = _mm_unpackhi_epi16((x),(y)); \ + __m128i out0##_l = _mm_madd_epi16(c0##lo, c0); \ + __m128i out0##_h = _mm_madd_epi16(c0##hi, c0); \ + __m128i out1##_l = _mm_madd_epi16(c0##lo, c1); \ + __m128i out1##_h = _mm_madd_epi16(c0##hi, c1) + + // out = in << 12 (in 16-bit, out 32-bit) + #define dct_widen(out, in) \ + __m128i out##_l = _mm_srai_epi32(_mm_unpacklo_epi16(_mm_setzero_si128(), (in)), 4); \ + __m128i out##_h = _mm_srai_epi32(_mm_unpackhi_epi16(_mm_setzero_si128(), (in)), 4) + + // wide add + #define dct_wadd(out, a, b) \ + __m128i out##_l = _mm_add_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_add_epi32(a##_h, b##_h) + + // wide sub + #define dct_wsub(out, a, b) \ + __m128i out##_l = _mm_sub_epi32(a##_l, b##_l); \ + __m128i out##_h = _mm_sub_epi32(a##_h, b##_h) + + // butterfly a/b, add bias, then shift by "s" and pack + #define dct_bfly32o(out0, out1, a,b,bias,s) \ + { \ + __m128i abiased_l = _mm_add_epi32(a##_l, bias); \ + __m128i abiased_h = _mm_add_epi32(a##_h, bias); \ + dct_wadd(sum, abiased, b); \ + dct_wsub(dif, abiased, b); \ + out0 = _mm_packs_epi32(_mm_srai_epi32(sum_l, s), _mm_srai_epi32(sum_h, s)); \ + out1 = _mm_packs_epi32(_mm_srai_epi32(dif_l, s), _mm_srai_epi32(dif_h, s)); \ + } + + // 8-bit interleave step (for transposes) + #define dct_interleave8(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi8(a, b); \ + b = _mm_unpackhi_epi8(tmp, b) + + // 16-bit interleave step (for transposes) + #define dct_interleave16(a, b) \ + tmp = a; \ + a = _mm_unpacklo_epi16(a, b); \ + b = _mm_unpackhi_epi16(tmp, b) + + #define dct_pass(bias,shift) \ + { \ + /* even part */ \ + dct_rot(t2e,t3e, row2,row6, rot0_0,rot0_1); \ + __m128i sum04 = _mm_add_epi16(row0, row4); \ + __m128i dif04 = _mm_sub_epi16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + dct_rot(y0o,y2o, row7,row3, rot2_0,rot2_1); \ + dct_rot(y1o,y3o, row5,row1, rot3_0,rot3_1); \ + __m128i sum17 = _mm_add_epi16(row1, row7); \ + __m128i sum35 = _mm_add_epi16(row3, row5); \ + dct_rot(y4o,y5o, sum17,sum35, rot1_0,rot1_1); \ + dct_wadd(x4, y0o, y4o); \ + dct_wadd(x5, y1o, y5o); \ + dct_wadd(x6, y2o, y5o); \ + dct_wadd(x7, y3o, y4o); \ + dct_bfly32o(row0,row7, x0,x7,bias,shift); \ + dct_bfly32o(row1,row6, x1,x6,bias,shift); \ + dct_bfly32o(row2,row5, x2,x5,bias,shift); \ + dct_bfly32o(row3,row4, x3,x4,bias,shift); \ + } + + __m128i rot0_0 = dct_const(stbi__f2f(0.5411961f), stbi__f2f(0.5411961f) + stbi__f2f(-1.847759065f)); + __m128i rot0_1 = dct_const(stbi__f2f(0.5411961f) + stbi__f2f( 0.765366865f), stbi__f2f(0.5411961f)); + __m128i rot1_0 = dct_const(stbi__f2f(1.175875602f) + stbi__f2f(-0.899976223f), stbi__f2f(1.175875602f)); + __m128i rot1_1 = dct_const(stbi__f2f(1.175875602f), stbi__f2f(1.175875602f) + stbi__f2f(-2.562915447f)); + __m128i rot2_0 = dct_const(stbi__f2f(-1.961570560f) + stbi__f2f( 0.298631336f), stbi__f2f(-1.961570560f)); + __m128i rot2_1 = dct_const(stbi__f2f(-1.961570560f), stbi__f2f(-1.961570560f) + stbi__f2f( 3.072711026f)); + __m128i rot3_0 = dct_const(stbi__f2f(-0.390180644f) + stbi__f2f( 2.053119869f), stbi__f2f(-0.390180644f)); + __m128i rot3_1 = dct_const(stbi__f2f(-0.390180644f), stbi__f2f(-0.390180644f) + stbi__f2f( 1.501321110f)); + + // rounding biases in column/row passes, see stbi__idct_block for explanation. + __m128i bias_0 = _mm_set1_epi32(512); + __m128i bias_1 = _mm_set1_epi32(65536 + (128<<17)); + + // load + row0 = _mm_load_si128((const __m128i *) (data + 0*8)); + row1 = _mm_load_si128((const __m128i *) (data + 1*8)); + row2 = _mm_load_si128((const __m128i *) (data + 2*8)); + row3 = _mm_load_si128((const __m128i *) (data + 3*8)); + row4 = _mm_load_si128((const __m128i *) (data + 4*8)); + row5 = _mm_load_si128((const __m128i *) (data + 5*8)); + row6 = _mm_load_si128((const __m128i *) (data + 6*8)); + row7 = _mm_load_si128((const __m128i *) (data + 7*8)); + + // column pass + dct_pass(bias_0, 10); + + { + // 16bit 8x8 transpose pass 1 + dct_interleave16(row0, row4); + dct_interleave16(row1, row5); + dct_interleave16(row2, row6); + dct_interleave16(row3, row7); + + // transpose pass 2 + dct_interleave16(row0, row2); + dct_interleave16(row1, row3); + dct_interleave16(row4, row6); + dct_interleave16(row5, row7); + + // transpose pass 3 + dct_interleave16(row0, row1); + dct_interleave16(row2, row3); + dct_interleave16(row4, row5); + dct_interleave16(row6, row7); + } + + // row pass + dct_pass(bias_1, 17); + + { + // pack + __m128i p0 = _mm_packus_epi16(row0, row1); // a0a1a2a3...a7b0b1b2b3...b7 + __m128i p1 = _mm_packus_epi16(row2, row3); + __m128i p2 = _mm_packus_epi16(row4, row5); + __m128i p3 = _mm_packus_epi16(row6, row7); + + // 8bit 8x8 transpose pass 1 + dct_interleave8(p0, p2); // a0e0a1e1... + dct_interleave8(p1, p3); // c0g0c1g1... + + // transpose pass 2 + dct_interleave8(p0, p1); // a0c0e0g0... + dct_interleave8(p2, p3); // b0d0f0h0... + + // transpose pass 3 + dct_interleave8(p0, p2); // a0b0c0d0... + dct_interleave8(p1, p3); // a4b4c4d4... + + // store + _mm_storel_epi64((__m128i *) out, p0); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p0, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p2); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p2, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p1); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p1, 0x4e)); out += out_stride; + _mm_storel_epi64((__m128i *) out, p3); out += out_stride; + _mm_storel_epi64((__m128i *) out, _mm_shuffle_epi32(p3, 0x4e)); + } + +#undef dct_const +#undef dct_rot +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_interleave8 +#undef dct_interleave16 +#undef dct_pass +} + +#endif // STBI_SSE2 + +#ifdef STBI_NEON + +// NEON integer IDCT. should produce bit-identical +// results to the generic C version. +static void stbi__idct_simd(stbi_uc *out, int out_stride, short data[64]) +{ + int16x8_t row0, row1, row2, row3, row4, row5, row6, row7; + + int16x4_t rot0_0 = vdup_n_s16(stbi__f2f(0.5411961f)); + int16x4_t rot0_1 = vdup_n_s16(stbi__f2f(-1.847759065f)); + int16x4_t rot0_2 = vdup_n_s16(stbi__f2f( 0.765366865f)); + int16x4_t rot1_0 = vdup_n_s16(stbi__f2f( 1.175875602f)); + int16x4_t rot1_1 = vdup_n_s16(stbi__f2f(-0.899976223f)); + int16x4_t rot1_2 = vdup_n_s16(stbi__f2f(-2.562915447f)); + int16x4_t rot2_0 = vdup_n_s16(stbi__f2f(-1.961570560f)); + int16x4_t rot2_1 = vdup_n_s16(stbi__f2f(-0.390180644f)); + int16x4_t rot3_0 = vdup_n_s16(stbi__f2f( 0.298631336f)); + int16x4_t rot3_1 = vdup_n_s16(stbi__f2f( 2.053119869f)); + int16x4_t rot3_2 = vdup_n_s16(stbi__f2f( 3.072711026f)); + int16x4_t rot3_3 = vdup_n_s16(stbi__f2f( 1.501321110f)); + +#define dct_long_mul(out, inq, coeff) \ + int32x4_t out##_l = vmull_s16(vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmull_s16(vget_high_s16(inq), coeff) + +#define dct_long_mac(out, acc, inq, coeff) \ + int32x4_t out##_l = vmlal_s16(acc##_l, vget_low_s16(inq), coeff); \ + int32x4_t out##_h = vmlal_s16(acc##_h, vget_high_s16(inq), coeff) + +#define dct_widen(out, inq) \ + int32x4_t out##_l = vshll_n_s16(vget_low_s16(inq), 12); \ + int32x4_t out##_h = vshll_n_s16(vget_high_s16(inq), 12) + +// wide add +#define dct_wadd(out, a, b) \ + int32x4_t out##_l = vaddq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vaddq_s32(a##_h, b##_h) + +// wide sub +#define dct_wsub(out, a, b) \ + int32x4_t out##_l = vsubq_s32(a##_l, b##_l); \ + int32x4_t out##_h = vsubq_s32(a##_h, b##_h) + +// butterfly a/b, then shift using "shiftop" by "s" and pack +#define dct_bfly32o(out0,out1, a,b,shiftop,s) \ + { \ + dct_wadd(sum, a, b); \ + dct_wsub(dif, a, b); \ + out0 = vcombine_s16(shiftop(sum_l, s), shiftop(sum_h, s)); \ + out1 = vcombine_s16(shiftop(dif_l, s), shiftop(dif_h, s)); \ + } + +#define dct_pass(shiftop, shift) \ + { \ + /* even part */ \ + int16x8_t sum26 = vaddq_s16(row2, row6); \ + dct_long_mul(p1e, sum26, rot0_0); \ + dct_long_mac(t2e, p1e, row6, rot0_1); \ + dct_long_mac(t3e, p1e, row2, rot0_2); \ + int16x8_t sum04 = vaddq_s16(row0, row4); \ + int16x8_t dif04 = vsubq_s16(row0, row4); \ + dct_widen(t0e, sum04); \ + dct_widen(t1e, dif04); \ + dct_wadd(x0, t0e, t3e); \ + dct_wsub(x3, t0e, t3e); \ + dct_wadd(x1, t1e, t2e); \ + dct_wsub(x2, t1e, t2e); \ + /* odd part */ \ + int16x8_t sum15 = vaddq_s16(row1, row5); \ + int16x8_t sum17 = vaddq_s16(row1, row7); \ + int16x8_t sum35 = vaddq_s16(row3, row5); \ + int16x8_t sum37 = vaddq_s16(row3, row7); \ + int16x8_t sumodd = vaddq_s16(sum17, sum35); \ + dct_long_mul(p5o, sumodd, rot1_0); \ + dct_long_mac(p1o, p5o, sum17, rot1_1); \ + dct_long_mac(p2o, p5o, sum35, rot1_2); \ + dct_long_mul(p3o, sum37, rot2_0); \ + dct_long_mul(p4o, sum15, rot2_1); \ + dct_wadd(sump13o, p1o, p3o); \ + dct_wadd(sump24o, p2o, p4o); \ + dct_wadd(sump23o, p2o, p3o); \ + dct_wadd(sump14o, p1o, p4o); \ + dct_long_mac(x4, sump13o, row7, rot3_0); \ + dct_long_mac(x5, sump24o, row5, rot3_1); \ + dct_long_mac(x6, sump23o, row3, rot3_2); \ + dct_long_mac(x7, sump14o, row1, rot3_3); \ + dct_bfly32o(row0,row7, x0,x7,shiftop,shift); \ + dct_bfly32o(row1,row6, x1,x6,shiftop,shift); \ + dct_bfly32o(row2,row5, x2,x5,shiftop,shift); \ + dct_bfly32o(row3,row4, x3,x4,shiftop,shift); \ + } + + // load + row0 = vld1q_s16(data + 0*8); + row1 = vld1q_s16(data + 1*8); + row2 = vld1q_s16(data + 2*8); + row3 = vld1q_s16(data + 3*8); + row4 = vld1q_s16(data + 4*8); + row5 = vld1q_s16(data + 5*8); + row6 = vld1q_s16(data + 6*8); + row7 = vld1q_s16(data + 7*8); + + // add DC bias + row0 = vaddq_s16(row0, vsetq_lane_s16(1024, vdupq_n_s16(0), 0)); + + // column pass + dct_pass(vrshrn_n_s32, 10); + + // 16bit 8x8 transpose + { +// these three map to a single VTRN.16, VTRN.32, and VSWP, respectively. +// whether compilers actually get this is another story, sadly. +#define dct_trn16(x, y) { int16x8x2_t t = vtrnq_s16(x, y); x = t.val[0]; y = t.val[1]; } +#define dct_trn32(x, y) { int32x4x2_t t = vtrnq_s32(vreinterpretq_s32_s16(x), vreinterpretq_s32_s16(y)); x = vreinterpretq_s16_s32(t.val[0]); y = vreinterpretq_s16_s32(t.val[1]); } +#define dct_trn64(x, y) { int16x8_t x0 = x; int16x8_t y0 = y; x = vcombine_s16(vget_low_s16(x0), vget_low_s16(y0)); y = vcombine_s16(vget_high_s16(x0), vget_high_s16(y0)); } + + // pass 1 + dct_trn16(row0, row1); // a0b0a2b2a4b4a6b6 + dct_trn16(row2, row3); + dct_trn16(row4, row5); + dct_trn16(row6, row7); + + // pass 2 + dct_trn32(row0, row2); // a0b0c0d0a4b4c4d4 + dct_trn32(row1, row3); + dct_trn32(row4, row6); + dct_trn32(row5, row7); + + // pass 3 + dct_trn64(row0, row4); // a0b0c0d0e0f0g0h0 + dct_trn64(row1, row5); + dct_trn64(row2, row6); + dct_trn64(row3, row7); + +#undef dct_trn16 +#undef dct_trn32 +#undef dct_trn64 + } + + // row pass + // vrshrn_n_s32 only supports shifts up to 16, we need + // 17. so do a non-rounding shift of 16 first then follow + // up with a rounding shift by 1. + dct_pass(vshrn_n_s32, 16); + + { + // pack and round + uint8x8_t p0 = vqrshrun_n_s16(row0, 1); + uint8x8_t p1 = vqrshrun_n_s16(row1, 1); + uint8x8_t p2 = vqrshrun_n_s16(row2, 1); + uint8x8_t p3 = vqrshrun_n_s16(row3, 1); + uint8x8_t p4 = vqrshrun_n_s16(row4, 1); + uint8x8_t p5 = vqrshrun_n_s16(row5, 1); + uint8x8_t p6 = vqrshrun_n_s16(row6, 1); + uint8x8_t p7 = vqrshrun_n_s16(row7, 1); + + // again, these can translate into one instruction, but often don't. +#define dct_trn8_8(x, y) { uint8x8x2_t t = vtrn_u8(x, y); x = t.val[0]; y = t.val[1]; } +#define dct_trn8_16(x, y) { uint16x4x2_t t = vtrn_u16(vreinterpret_u16_u8(x), vreinterpret_u16_u8(y)); x = vreinterpret_u8_u16(t.val[0]); y = vreinterpret_u8_u16(t.val[1]); } +#define dct_trn8_32(x, y) { uint32x2x2_t t = vtrn_u32(vreinterpret_u32_u8(x), vreinterpret_u32_u8(y)); x = vreinterpret_u8_u32(t.val[0]); y = vreinterpret_u8_u32(t.val[1]); } + + // sadly can't use interleaved stores here since we only write + // 8 bytes to each scan line! + + // 8x8 8-bit transpose pass 1 + dct_trn8_8(p0, p1); + dct_trn8_8(p2, p3); + dct_trn8_8(p4, p5); + dct_trn8_8(p6, p7); + + // pass 2 + dct_trn8_16(p0, p2); + dct_trn8_16(p1, p3); + dct_trn8_16(p4, p6); + dct_trn8_16(p5, p7); + + // pass 3 + dct_trn8_32(p0, p4); + dct_trn8_32(p1, p5); + dct_trn8_32(p2, p6); + dct_trn8_32(p3, p7); + + // store + vst1_u8(out, p0); out += out_stride; + vst1_u8(out, p1); out += out_stride; + vst1_u8(out, p2); out += out_stride; + vst1_u8(out, p3); out += out_stride; + vst1_u8(out, p4); out += out_stride; + vst1_u8(out, p5); out += out_stride; + vst1_u8(out, p6); out += out_stride; + vst1_u8(out, p7); + +#undef dct_trn8_8 +#undef dct_trn8_16 +#undef dct_trn8_32 + } + +#undef dct_long_mul +#undef dct_long_mac +#undef dct_widen +#undef dct_wadd +#undef dct_wsub +#undef dct_bfly32o +#undef dct_pass +} + +#endif // STBI_NEON + +#define STBI__MARKER_none 0xff +// if there's a pending marker from the entropy stream, return that +// otherwise, fetch from the stream and get a marker. if there's no +// marker, return 0xff, which is never a valid marker value +static stbi_uc stbi__get_marker(stbi__jpeg *j) +{ + stbi_uc x; + if (j->marker != STBI__MARKER_none) { x = j->marker; j->marker = STBI__MARKER_none; return x; } + x = stbi__get8(j->s); + if (x != 0xff) return STBI__MARKER_none; + while (x == 0xff) + x = stbi__get8(j->s); // consume repeated 0xff fill bytes + return x; +} + +// in each scan, we'll have scan_n components, and the order +// of the components is specified by order[] +#define STBI__RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) + +// after a restart interval, stbi__jpeg_reset the entropy decoder and +// the dc prediction +static void stbi__jpeg_reset(stbi__jpeg *j) +{ + j->code_bits = 0; + j->code_buffer = 0; + j->nomore = 0; + j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = j->img_comp[3].dc_pred = 0; + j->marker = STBI__MARKER_none; + j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; + j->eob_run = 0; + // no more than 1<<31 MCUs if no restart_interal? that's plenty safe, + // since we don't even allow 1<<30 pixels +} + +static int stbi__parse_entropy_coded_data(stbi__jpeg *z) +{ + stbi__jpeg_reset(z); + if (!z->progressive) { + if (z->scan_n == 1) { + int i,j; + STBI_SIMD_ALIGN(short, data[64]); + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + // if it's NOT a restart, then just bail, so we get corrupt data + // rather than no data + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } else { // interleaved + int i,j,k,x,y; + STBI_SIMD_ALIGN(short, data[64]); + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x)*8; + int y2 = (j*z->img_comp[n].v + y)*8; + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+ha, z->fast_ac[ha], n, z->dequant[z->img_comp[n].tq])) return 0; + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data); + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } else { + if (z->scan_n == 1) { + int i,j; + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); + if (z->spec_start == 0) { + if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } else { + int ha = z->img_comp[n].ha; + if (!stbi__jpeg_decode_block_prog_ac(z, data, &z->huff_ac[ha], z->fast_ac[ha])) + return 0; + } + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } else { // interleaved + int i,j,k,x,y; + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x); + int y2 = (j*z->img_comp[n].v + y); + short *data = z->img_comp[n].coeff + 64 * (x2 + y2 * z->img_comp[n].coeff_w); + if (!stbi__jpeg_decode_block_prog_dc(z, data, &z->huff_dc[z->img_comp[n].hd], n)) + return 0; + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) stbi__grow_buffer_unsafe(z); + if (!STBI__RESTART(z->marker)) return 1; + stbi__jpeg_reset(z); + } + } + } + return 1; + } + } +} + +static void stbi__jpeg_dequantize(short *data, stbi__uint16 *dequant) +{ + int i; + for (i=0; i < 64; ++i) + data[i] *= dequant[i]; +} + +static void stbi__jpeg_finish(stbi__jpeg *z) +{ + if (z->progressive) { + // dequantize and idct the data + int i,j,n; + for (n=0; n < z->s->img_n; ++n) { + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + short *data = z->img_comp[n].coeff + 64 * (i + j * z->img_comp[n].coeff_w); + stbi__jpeg_dequantize(data, z->dequant[z->img_comp[n].tq]); + z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); + } + } + } + } +} + +static int stbi__process_marker(stbi__jpeg *z, int m) +{ + int L; + switch (m) { + case STBI__MARKER_none: // no marker found + return stbi__err("expected marker","Corrupt JPEG"); + + case 0xDD: // DRI - specify restart interval + if (stbi__get16be(z->s) != 4) return stbi__err("bad DRI len","Corrupt JPEG"); + z->restart_interval = stbi__get16be(z->s); + return 1; + + case 0xDB: // DQT - define quantization table + L = stbi__get16be(z->s)-2; + while (L > 0) { + int q = stbi__get8(z->s); + int p = q >> 4, sixteen = (p != 0); + int t = q & 15,i; + if (p != 0 && p != 1) return stbi__err("bad DQT type","Corrupt JPEG"); + if (t > 3) return stbi__err("bad DQT table","Corrupt JPEG"); + + for (i=0; i < 64; ++i) + z->dequant[t][stbi__jpeg_dezigzag[i]] = (stbi__uint16)(sixteen ? stbi__get16be(z->s) : stbi__get8(z->s)); + L -= (sixteen ? 129 : 65); + } + return L==0; + + case 0xC4: // DHT - define huffman table + L = stbi__get16be(z->s)-2; + while (L > 0) { + stbi_uc *v; + int sizes[16],i,n=0; + int q = stbi__get8(z->s); + int tc = q >> 4; + int th = q & 15; + if (tc > 1 || th > 3) return stbi__err("bad DHT header","Corrupt JPEG"); + for (i=0; i < 16; ++i) { + sizes[i] = stbi__get8(z->s); + n += sizes[i]; + } + if(n > 256) return stbi__err("bad DHT header","Corrupt JPEG"); // Loop over i < n would write past end of values! + L -= 17; + if (tc == 0) { + if (!stbi__build_huffman(z->huff_dc+th, sizes)) return 0; + v = z->huff_dc[th].values; + } else { + if (!stbi__build_huffman(z->huff_ac+th, sizes)) return 0; + v = z->huff_ac[th].values; + } + for (i=0; i < n; ++i) + v[i] = stbi__get8(z->s); + if (tc != 0) + stbi__build_fast_ac(z->fast_ac[th], z->huff_ac + th); + L -= n; + } + return L==0; + } + + // check for comment block or APP blocks + if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { + L = stbi__get16be(z->s); + if (L < 2) { + if (m == 0xFE) + return stbi__err("bad COM len","Corrupt JPEG"); + else + return stbi__err("bad APP len","Corrupt JPEG"); + } + L -= 2; + + if (m == 0xE0 && L >= 5) { // JFIF APP0 segment + static const unsigned char tag[5] = {'J','F','I','F','\0'}; + int ok = 1; + int i; + for (i=0; i < 5; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 5; + if (ok) + z->jfif = 1; + } else if (m == 0xEE && L >= 12) { // Adobe APP14 segment + static const unsigned char tag[6] = {'A','d','o','b','e','\0'}; + int ok = 1; + int i; + for (i=0; i < 6; ++i) + if (stbi__get8(z->s) != tag[i]) + ok = 0; + L -= 6; + if (ok) { + stbi__get8(z->s); // version + stbi__get16be(z->s); // flags0 + stbi__get16be(z->s); // flags1 + z->app14_color_transform = stbi__get8(z->s); // color transform + L -= 6; + } + } + + stbi__skip(z->s, L); + return 1; + } + + return stbi__err("unknown marker","Corrupt JPEG"); +} + +// after we see SOS +static int stbi__process_scan_header(stbi__jpeg *z) +{ + int i; + int Ls = stbi__get16be(z->s); + z->scan_n = stbi__get8(z->s); + if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return stbi__err("bad SOS component count","Corrupt JPEG"); + if (Ls != 6+2*z->scan_n) return stbi__err("bad SOS len","Corrupt JPEG"); + for (i=0; i < z->scan_n; ++i) { + int id = stbi__get8(z->s), which; + int q = stbi__get8(z->s); + for (which = 0; which < z->s->img_n; ++which) + if (z->img_comp[which].id == id) + break; + if (which == z->s->img_n) return 0; // no match + z->img_comp[which].hd = q >> 4; if (z->img_comp[which].hd > 3) return stbi__err("bad DC huff","Corrupt JPEG"); + z->img_comp[which].ha = q & 15; if (z->img_comp[which].ha > 3) return stbi__err("bad AC huff","Corrupt JPEG"); + z->order[i] = which; + } + + { + int aa; + z->spec_start = stbi__get8(z->s); + z->spec_end = stbi__get8(z->s); // should be 63, but might be 0 + aa = stbi__get8(z->s); + z->succ_high = (aa >> 4); + z->succ_low = (aa & 15); + if (z->progressive) { + if (z->spec_start > 63 || z->spec_end > 63 || z->spec_start > z->spec_end || z->succ_high > 13 || z->succ_low > 13) + return stbi__err("bad SOS", "Corrupt JPEG"); + } else { + if (z->spec_start != 0) return stbi__err("bad SOS","Corrupt JPEG"); + if (z->succ_high != 0 || z->succ_low != 0) return stbi__err("bad SOS","Corrupt JPEG"); + z->spec_end = 63; + } + } + + return 1; +} + +static int stbi__free_jpeg_components(stbi__jpeg *z, int ncomp, int why) +{ + int i; + for (i=0; i < ncomp; ++i) { + if (z->img_comp[i].raw_data) { + STBI_FREE(z->img_comp[i].raw_data); + z->img_comp[i].raw_data = NULL; + z->img_comp[i].data = NULL; + } + if (z->img_comp[i].raw_coeff) { + STBI_FREE(z->img_comp[i].raw_coeff); + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].coeff = 0; + } + if (z->img_comp[i].linebuf) { + STBI_FREE(z->img_comp[i].linebuf); + z->img_comp[i].linebuf = NULL; + } + } + return why; +} + +static int stbi__process_frame_header(stbi__jpeg *z, int scan) +{ + stbi__context *s = z->s; + int Lf,p,i,q, h_max=1,v_max=1,c; + Lf = stbi__get16be(s); if (Lf < 11) return stbi__err("bad SOF len","Corrupt JPEG"); // JPEG + p = stbi__get8(s); if (p != 8) return stbi__err("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline + s->img_y = stbi__get16be(s); if (s->img_y == 0) return stbi__err("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG + s->img_x = stbi__get16be(s); if (s->img_x == 0) return stbi__err("0 width","Corrupt JPEG"); // JPEG requires + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + c = stbi__get8(s); + if (c != 3 && c != 1 && c != 4) return stbi__err("bad component count","Corrupt JPEG"); + s->img_n = c; + for (i=0; i < c; ++i) { + z->img_comp[i].data = NULL; + z->img_comp[i].linebuf = NULL; + } + + if (Lf != 8+3*s->img_n) return stbi__err("bad SOF len","Corrupt JPEG"); + + z->rgb = 0; + for (i=0; i < s->img_n; ++i) { + static const unsigned char rgb[3] = { 'R', 'G', 'B' }; + z->img_comp[i].id = stbi__get8(s); + if (s->img_n == 3 && z->img_comp[i].id == rgb[i]) + ++z->rgb; + q = stbi__get8(s); + z->img_comp[i].h = (q >> 4); if (!z->img_comp[i].h || z->img_comp[i].h > 4) return stbi__err("bad H","Corrupt JPEG"); + z->img_comp[i].v = q & 15; if (!z->img_comp[i].v || z->img_comp[i].v > 4) return stbi__err("bad V","Corrupt JPEG"); + z->img_comp[i].tq = stbi__get8(s); if (z->img_comp[i].tq > 3) return stbi__err("bad TQ","Corrupt JPEG"); + } + + if (scan != STBI__SCAN_load) return 1; + + if (!stbi__mad3sizes_valid(s->img_x, s->img_y, s->img_n, 0)) return stbi__err("too large", "Image too large to decode"); + + for (i=0; i < s->img_n; ++i) { + if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h; + if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; + } + + // check that plane subsampling factors are integer ratios; our resamplers can't deal with fractional ratios + // and I've never seen a non-corrupted JPEG file actually use them + for (i=0; i < s->img_n; ++i) { + if (h_max % z->img_comp[i].h != 0) return stbi__err("bad H","Corrupt JPEG"); + if (v_max % z->img_comp[i].v != 0) return stbi__err("bad V","Corrupt JPEG"); + } + + // compute interleaved mcu info + z->img_h_max = h_max; + z->img_v_max = v_max; + z->img_mcu_w = h_max * 8; + z->img_mcu_h = v_max * 8; + // these sizes can't be more than 17 bits + z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w; + z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h; + + for (i=0; i < s->img_n; ++i) { + // number of effective pixels (e.g. for non-interleaved MCU) + z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max; + z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max; + // to simplify generation, we'll allocate enough memory to decode + // the bogus oversized data from using interleaved MCUs and their + // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't + // discard the extra data until colorspace conversion + // + // img_mcu_x, img_mcu_y: <=17 bits; comp[i].h and .v are <=4 (checked earlier) + // so these muls can't overflow with 32-bit ints (which we require) + z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; + z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; + z->img_comp[i].coeff = 0; + z->img_comp[i].raw_coeff = 0; + z->img_comp[i].linebuf = NULL; + z->img_comp[i].raw_data = stbi__malloc_mad2(z->img_comp[i].w2, z->img_comp[i].h2, 15); + if (z->img_comp[i].raw_data == NULL) + return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); + // align blocks for idct using mmx/sse + z->img_comp[i].data = (stbi_uc*) (((size_t) z->img_comp[i].raw_data + 15) & ~15); + if (z->progressive) { + // w2, h2 are multiples of 8 (see above) + z->img_comp[i].coeff_w = z->img_comp[i].w2 / 8; + z->img_comp[i].coeff_h = z->img_comp[i].h2 / 8; + z->img_comp[i].raw_coeff = stbi__malloc_mad3(z->img_comp[i].w2, z->img_comp[i].h2, sizeof(short), 15); + if (z->img_comp[i].raw_coeff == NULL) + return stbi__free_jpeg_components(z, i+1, stbi__err("outofmem", "Out of memory")); + z->img_comp[i].coeff = (short*) (((size_t) z->img_comp[i].raw_coeff + 15) & ~15); + } + } + + return 1; +} + +// use comparisons since in some cases we handle more than one case (e.g. SOF) +#define stbi__DNL(x) ((x) == 0xdc) +#define stbi__SOI(x) ((x) == 0xd8) +#define stbi__EOI(x) ((x) == 0xd9) +#define stbi__SOF(x) ((x) == 0xc0 || (x) == 0xc1 || (x) == 0xc2) +#define stbi__SOS(x) ((x) == 0xda) + +#define stbi__SOF_progressive(x) ((x) == 0xc2) + +static int stbi__decode_jpeg_header(stbi__jpeg *z, int scan) +{ + int m; + z->jfif = 0; + z->app14_color_transform = -1; // valid values are 0,1,2 + z->marker = STBI__MARKER_none; // initialize cached marker to empty + m = stbi__get_marker(z); + if (!stbi__SOI(m)) return stbi__err("no SOI","Corrupt JPEG"); + if (scan == STBI__SCAN_type) return 1; + m = stbi__get_marker(z); + while (!stbi__SOF(m)) { + if (!stbi__process_marker(z,m)) return 0; + m = stbi__get_marker(z); + while (m == STBI__MARKER_none) { + // some files have extra padding after their blocks, so ok, we'll scan + if (stbi__at_eof(z->s)) return stbi__err("no SOF", "Corrupt JPEG"); + m = stbi__get_marker(z); + } + } + z->progressive = stbi__SOF_progressive(m); + if (!stbi__process_frame_header(z, scan)) return 0; + return 1; +} + +static stbi_uc stbi__skip_jpeg_junk_at_end(stbi__jpeg *j) +{ + // some JPEGs have junk at end, skip over it but if we find what looks + // like a valid marker, resume there + while (!stbi__at_eof(j->s)) { + stbi_uc x = stbi__get8(j->s); + while (x == 0xff) { // might be a marker + if (stbi__at_eof(j->s)) return STBI__MARKER_none; + x = stbi__get8(j->s); + if (x != 0x00 && x != 0xff) { + // not a stuffed zero or lead-in to another marker, looks + // like an actual marker, return it + return x; + } + // stuffed zero has x=0 now which ends the loop, meaning we go + // back to regular scan loop. + // repeated 0xff keeps trying to read the next byte of the marker. + } + } + return STBI__MARKER_none; +} + +// decode image to YCbCr format +static int stbi__decode_jpeg_image(stbi__jpeg *j) +{ + int m; + for (m = 0; m < 4; m++) { + j->img_comp[m].raw_data = NULL; + j->img_comp[m].raw_coeff = NULL; + } + j->restart_interval = 0; + if (!stbi__decode_jpeg_header(j, STBI__SCAN_load)) return 0; + m = stbi__get_marker(j); + while (!stbi__EOI(m)) { + if (stbi__SOS(m)) { + if (!stbi__process_scan_header(j)) return 0; + if (!stbi__parse_entropy_coded_data(j)) return 0; + if (j->marker == STBI__MARKER_none ) { + j->marker = stbi__skip_jpeg_junk_at_end(j); + // if we reach eof without hitting a marker, stbi__get_marker() below will fail and we'll eventually return 0 + } + m = stbi__get_marker(j); + if (STBI__RESTART(m)) + m = stbi__get_marker(j); + } else if (stbi__DNL(m)) { + int Ld = stbi__get16be(j->s); + stbi__uint32 NL = stbi__get16be(j->s); + if (Ld != 4) return stbi__err("bad DNL len", "Corrupt JPEG"); + if (NL != j->s->img_y) return stbi__err("bad DNL height", "Corrupt JPEG"); + m = stbi__get_marker(j); + } else { + if (!stbi__process_marker(j, m)) return 1; + m = stbi__get_marker(j); + } + } + if (j->progressive) + stbi__jpeg_finish(j); + return 1; +} + +// static jfif-centered resampling (across block boundaries) + +typedef stbi_uc *(*resample_row_func)(stbi_uc *out, stbi_uc *in0, stbi_uc *in1, + int w, int hs); + +#define stbi__div4(x) ((stbi_uc) ((x) >> 2)) + +static stbi_uc *resample_row_1(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + STBI_NOTUSED(out); + STBI_NOTUSED(in_far); + STBI_NOTUSED(w); + STBI_NOTUSED(hs); + return in_near; +} + +static stbi_uc* stbi__resample_row_v_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate two samples vertically for every one in input + int i; + STBI_NOTUSED(hs); + for (i=0; i < w; ++i) + out[i] = stbi__div4(3*in_near[i] + in_far[i] + 2); + return out; +} + +static stbi_uc* stbi__resample_row_h_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate two samples horizontally for every one in input + int i; + stbi_uc *input = in_near; + + if (w == 1) { + // if only one sample, can't do any interpolation + out[0] = out[1] = input[0]; + return out; + } + + out[0] = input[0]; + out[1] = stbi__div4(input[0]*3 + input[1] + 2); + for (i=1; i < w-1; ++i) { + int n = 3*input[i]+2; + out[i*2+0] = stbi__div4(n+input[i-1]); + out[i*2+1] = stbi__div4(n+input[i+1]); + } + out[i*2+0] = stbi__div4(input[w-2]*3 + input[w-1] + 2); + out[i*2+1] = input[w-1]; + + STBI_NOTUSED(in_far); + STBI_NOTUSED(hs); + + return out; +} + +#define stbi__div16(x) ((stbi_uc) ((x) >> 4)) + +static stbi_uc *stbi__resample_row_hv_2(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i,t0,t1; + if (w == 1) { + out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + out[0] = stbi__div4(t1+2); + for (i=1; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = stbi__div16(3*t0 + t1 + 8); + out[i*2 ] = stbi__div16(3*t1 + t0 + 8); + } + out[w*2-1] = stbi__div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static stbi_uc *stbi__resample_row_hv_2_simd(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i=0,t0,t1; + + if (w == 1) { + out[0] = out[1] = stbi__div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + // process groups of 8 pixels for as long as we can. + // note we can't handle the last pixel in a row in this loop + // because we need to handle the filter boundary conditions. + for (; i < ((w-1) & ~7); i += 8) { +#if defined(STBI_SSE2) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + __m128i zero = _mm_setzero_si128(); + __m128i farb = _mm_loadl_epi64((__m128i *) (in_far + i)); + __m128i nearb = _mm_loadl_epi64((__m128i *) (in_near + i)); + __m128i farw = _mm_unpacklo_epi8(farb, zero); + __m128i nearw = _mm_unpacklo_epi8(nearb, zero); + __m128i diff = _mm_sub_epi16(farw, nearw); + __m128i nears = _mm_slli_epi16(nearw, 2); + __m128i curr = _mm_add_epi16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + __m128i prv0 = _mm_slli_si128(curr, 2); + __m128i nxt0 = _mm_srli_si128(curr, 2); + __m128i prev = _mm_insert_epi16(prv0, t1, 0); + __m128i next = _mm_insert_epi16(nxt0, 3*in_near[i+8] + in_far[i+8], 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + __m128i bias = _mm_set1_epi16(8); + __m128i curs = _mm_slli_epi16(curr, 2); + __m128i prvd = _mm_sub_epi16(prev, curr); + __m128i nxtd = _mm_sub_epi16(next, curr); + __m128i curb = _mm_add_epi16(curs, bias); + __m128i even = _mm_add_epi16(prvd, curb); + __m128i odd = _mm_add_epi16(nxtd, curb); + + // interleave even and odd pixels, then undo scaling. + __m128i int0 = _mm_unpacklo_epi16(even, odd); + __m128i int1 = _mm_unpackhi_epi16(even, odd); + __m128i de0 = _mm_srli_epi16(int0, 4); + __m128i de1 = _mm_srli_epi16(int1, 4); + + // pack and write output + __m128i outv = _mm_packus_epi16(de0, de1); + _mm_storeu_si128((__m128i *) (out + i*2), outv); +#elif defined(STBI_NEON) + // load and perform the vertical filtering pass + // this uses 3*x + y = 4*x + (y - x) + uint8x8_t farb = vld1_u8(in_far + i); + uint8x8_t nearb = vld1_u8(in_near + i); + int16x8_t diff = vreinterpretq_s16_u16(vsubl_u8(farb, nearb)); + int16x8_t nears = vreinterpretq_s16_u16(vshll_n_u8(nearb, 2)); + int16x8_t curr = vaddq_s16(nears, diff); // current row + + // horizontal filter works the same based on shifted vers of current + // row. "prev" is current row shifted right by 1 pixel; we need to + // insert the previous pixel value (from t1). + // "next" is current row shifted left by 1 pixel, with first pixel + // of next block of 8 pixels added in. + int16x8_t prv0 = vextq_s16(curr, curr, 7); + int16x8_t nxt0 = vextq_s16(curr, curr, 1); + int16x8_t prev = vsetq_lane_s16(t1, prv0, 0); + int16x8_t next = vsetq_lane_s16(3*in_near[i+8] + in_far[i+8], nxt0, 7); + + // horizontal filter, polyphase implementation since it's convenient: + // even pixels = 3*cur + prev = cur*4 + (prev - cur) + // odd pixels = 3*cur + next = cur*4 + (next - cur) + // note the shared term. + int16x8_t curs = vshlq_n_s16(curr, 2); + int16x8_t prvd = vsubq_s16(prev, curr); + int16x8_t nxtd = vsubq_s16(next, curr); + int16x8_t even = vaddq_s16(curs, prvd); + int16x8_t odd = vaddq_s16(curs, nxtd); + + // undo scaling and round, then store with even/odd phases interleaved + uint8x8x2_t o; + o.val[0] = vqrshrun_n_s16(even, 4); + o.val[1] = vqrshrun_n_s16(odd, 4); + vst2_u8(out + i*2, o); +#endif + + // "previous" value for next iter + t1 = 3*in_near[i+7] + in_far[i+7]; + } + + t0 = t1; + t1 = 3*in_near[i] + in_far[i]; + out[i*2] = stbi__div16(3*t1 + t0 + 8); + + for (++i; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = stbi__div16(3*t0 + t1 + 8); + out[i*2 ] = stbi__div16(3*t1 + t0 + 8); + } + out[w*2-1] = stbi__div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} +#endif + +static stbi_uc *stbi__resample_row_generic(stbi_uc *out, stbi_uc *in_near, stbi_uc *in_far, int w, int hs) +{ + // resample with nearest-neighbor + int i,j; + STBI_NOTUSED(in_far); + for (i=0; i < w; ++i) + for (j=0; j < hs; ++j) + out[i*hs+j] = in_near[i]; + return out; +} + +// this is a reduced-precision calculation of YCbCr-to-RGB introduced +// to make sure the code produces the same results in both SIMD and scalar +#define stbi__float2fixed(x) (((int) ((x) * 4096.0f + 0.5f)) << 8) +static void stbi__YCbCr_to_RGB_row(stbi_uc *out, const stbi_uc *y, const stbi_uc *pcb, const stbi_uc *pcr, int count, int step) +{ + int i; + for (i=0; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1<<19); // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr* stbi__float2fixed(1.40200f); + g = y_fixed + (cr*-stbi__float2fixed(0.71414f)) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb* stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} + +#if defined(STBI_SSE2) || defined(STBI_NEON) +static void stbi__YCbCr_to_RGB_simd(stbi_uc *out, stbi_uc const *y, stbi_uc const *pcb, stbi_uc const *pcr, int count, int step) +{ + int i = 0; + +#ifdef STBI_SSE2 + // step == 3 is pretty ugly on the final interleave, and i'm not convinced + // it's useful in practice (you wouldn't use it for textures, for example). + // so just accelerate step == 4 case. + if (step == 4) { + // this is a fairly straightforward implementation and not super-optimized. + __m128i signflip = _mm_set1_epi8(-0x80); + __m128i cr_const0 = _mm_set1_epi16( (short) ( 1.40200f*4096.0f+0.5f)); + __m128i cr_const1 = _mm_set1_epi16( - (short) ( 0.71414f*4096.0f+0.5f)); + __m128i cb_const0 = _mm_set1_epi16( - (short) ( 0.34414f*4096.0f+0.5f)); + __m128i cb_const1 = _mm_set1_epi16( (short) ( 1.77200f*4096.0f+0.5f)); + __m128i y_bias = _mm_set1_epi8((char) (unsigned char) 128); + __m128i xw = _mm_set1_epi16(255); // alpha channel + + for (; i+7 < count; i += 8) { + // load + __m128i y_bytes = _mm_loadl_epi64((__m128i *) (y+i)); + __m128i cr_bytes = _mm_loadl_epi64((__m128i *) (pcr+i)); + __m128i cb_bytes = _mm_loadl_epi64((__m128i *) (pcb+i)); + __m128i cr_biased = _mm_xor_si128(cr_bytes, signflip); // -128 + __m128i cb_biased = _mm_xor_si128(cb_bytes, signflip); // -128 + + // unpack to short (and left-shift cr, cb by 8) + __m128i yw = _mm_unpacklo_epi8(y_bias, y_bytes); + __m128i crw = _mm_unpacklo_epi8(_mm_setzero_si128(), cr_biased); + __m128i cbw = _mm_unpacklo_epi8(_mm_setzero_si128(), cb_biased); + + // color transform + __m128i yws = _mm_srli_epi16(yw, 4); + __m128i cr0 = _mm_mulhi_epi16(cr_const0, crw); + __m128i cb0 = _mm_mulhi_epi16(cb_const0, cbw); + __m128i cb1 = _mm_mulhi_epi16(cbw, cb_const1); + __m128i cr1 = _mm_mulhi_epi16(crw, cr_const1); + __m128i rws = _mm_add_epi16(cr0, yws); + __m128i gwt = _mm_add_epi16(cb0, yws); + __m128i bws = _mm_add_epi16(yws, cb1); + __m128i gws = _mm_add_epi16(gwt, cr1); + + // descale + __m128i rw = _mm_srai_epi16(rws, 4); + __m128i bw = _mm_srai_epi16(bws, 4); + __m128i gw = _mm_srai_epi16(gws, 4); + + // back to byte, set up for transpose + __m128i brb = _mm_packus_epi16(rw, bw); + __m128i gxb = _mm_packus_epi16(gw, xw); + + // transpose to interleave channels + __m128i t0 = _mm_unpacklo_epi8(brb, gxb); + __m128i t1 = _mm_unpackhi_epi8(brb, gxb); + __m128i o0 = _mm_unpacklo_epi16(t0, t1); + __m128i o1 = _mm_unpackhi_epi16(t0, t1); + + // store + _mm_storeu_si128((__m128i *) (out + 0), o0); + _mm_storeu_si128((__m128i *) (out + 16), o1); + out += 32; + } + } +#endif + +#ifdef STBI_NEON + // in this version, step=3 support would be easy to add. but is there demand? + if (step == 4) { + // this is a fairly straightforward implementation and not super-optimized. + uint8x8_t signflip = vdup_n_u8(0x80); + int16x8_t cr_const0 = vdupq_n_s16( (short) ( 1.40200f*4096.0f+0.5f)); + int16x8_t cr_const1 = vdupq_n_s16( - (short) ( 0.71414f*4096.0f+0.5f)); + int16x8_t cb_const0 = vdupq_n_s16( - (short) ( 0.34414f*4096.0f+0.5f)); + int16x8_t cb_const1 = vdupq_n_s16( (short) ( 1.77200f*4096.0f+0.5f)); + + for (; i+7 < count; i += 8) { + // load + uint8x8_t y_bytes = vld1_u8(y + i); + uint8x8_t cr_bytes = vld1_u8(pcr + i); + uint8x8_t cb_bytes = vld1_u8(pcb + i); + int8x8_t cr_biased = vreinterpret_s8_u8(vsub_u8(cr_bytes, signflip)); + int8x8_t cb_biased = vreinterpret_s8_u8(vsub_u8(cb_bytes, signflip)); + + // expand to s16 + int16x8_t yws = vreinterpretq_s16_u16(vshll_n_u8(y_bytes, 4)); + int16x8_t crw = vshll_n_s8(cr_biased, 7); + int16x8_t cbw = vshll_n_s8(cb_biased, 7); + + // color transform + int16x8_t cr0 = vqdmulhq_s16(crw, cr_const0); + int16x8_t cb0 = vqdmulhq_s16(cbw, cb_const0); + int16x8_t cr1 = vqdmulhq_s16(crw, cr_const1); + int16x8_t cb1 = vqdmulhq_s16(cbw, cb_const1); + int16x8_t rws = vaddq_s16(yws, cr0); + int16x8_t gws = vaddq_s16(vaddq_s16(yws, cb0), cr1); + int16x8_t bws = vaddq_s16(yws, cb1); + + // undo scaling, round, convert to byte + uint8x8x4_t o; + o.val[0] = vqrshrun_n_s16(rws, 4); + o.val[1] = vqrshrun_n_s16(gws, 4); + o.val[2] = vqrshrun_n_s16(bws, 4); + o.val[3] = vdup_n_u8(255); + + // store, interleaving r/g/b/a + vst4_u8(out, o); + out += 8*4; + } + } +#endif + + for (; i < count; ++i) { + int y_fixed = (y[i] << 20) + (1<<19); // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr* stbi__float2fixed(1.40200f); + g = y_fixed + cr*-stbi__float2fixed(0.71414f) + ((cb*-stbi__float2fixed(0.34414f)) & 0xffff0000); + b = y_fixed + cb* stbi__float2fixed(1.77200f); + r >>= 20; + g >>= 20; + b >>= 20; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (stbi_uc)r; + out[1] = (stbi_uc)g; + out[2] = (stbi_uc)b; + out[3] = 255; + out += step; + } +} +#endif + +// set up the kernels +static void stbi__setup_jpeg(stbi__jpeg *j) +{ + j->idct_block_kernel = stbi__idct_block; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_row; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2; + +#ifdef STBI_SSE2 + if (stbi__sse2_available()) { + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; + } +#endif + +#ifdef STBI_NEON + j->idct_block_kernel = stbi__idct_simd; + j->YCbCr_to_RGB_kernel = stbi__YCbCr_to_RGB_simd; + j->resample_row_hv_2_kernel = stbi__resample_row_hv_2_simd; +#endif +} + +// clean up the temporary component buffers +static void stbi__cleanup_jpeg(stbi__jpeg *j) +{ + stbi__free_jpeg_components(j, j->s->img_n, 0); +} + +typedef struct +{ + resample_row_func resample; + stbi_uc *line0,*line1; + int hs,vs; // expansion factor in each axis + int w_lores; // horizontal pixels pre-expansion + int ystep; // how far through vertical expansion we are + int ypos; // which pre-expansion row we're on +} stbi__resample; + +// fast 0..255 * 0..255 => 0..255 rounded multiplication +static stbi_uc stbi__blinn_8x8(stbi_uc x, stbi_uc y) +{ + unsigned int t = x*y + 128; + return (stbi_uc) ((t + (t >>8)) >> 8); +} + +static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp, int req_comp) +{ + int n, decode_n, is_rgb; + z->s->img_n = 0; // make stbi__cleanup_jpeg safe + + // validate req_comp + if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); + + // load a jpeg image from whichever source, but leave in YCbCr format + if (!stbi__decode_jpeg_image(z)) { stbi__cleanup_jpeg(z); return NULL; } + + // determine actual number of components to generate + n = req_comp ? req_comp : z->s->img_n >= 3 ? 3 : 1; + + is_rgb = z->s->img_n == 3 && (z->rgb == 3 || (z->app14_color_transform == 0 && !z->jfif)); + + if (z->s->img_n == 3 && n < 3 && !is_rgb) + decode_n = 1; + else + decode_n = z->s->img_n; + + // nothing to do if no components requested; check this now to avoid + // accessing uninitialized coutput[0] later + if (decode_n <= 0) { stbi__cleanup_jpeg(z); return NULL; } + + // resample and color-convert + { + int k; + unsigned int i,j; + stbi_uc *output; + stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL }; + + stbi__resample res_comp[4]; + + for (k=0; k < decode_n; ++k) { + stbi__resample *r = &res_comp[k]; + + // allocate line buffer big enough for upsampling off the edges + // with upsample factor of 4 + z->img_comp[k].linebuf = (stbi_uc *) stbi__malloc(z->s->img_x + 3); + if (!z->img_comp[k].linebuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + + r->hs = z->img_h_max / z->img_comp[k].h; + r->vs = z->img_v_max / z->img_comp[k].v; + r->ystep = r->vs >> 1; + r->w_lores = (z->s->img_x + r->hs-1) / r->hs; + r->ypos = 0; + r->line0 = r->line1 = z->img_comp[k].data; + + if (r->hs == 1 && r->vs == 1) r->resample = resample_row_1; + else if (r->hs == 1 && r->vs == 2) r->resample = stbi__resample_row_v_2; + else if (r->hs == 2 && r->vs == 1) r->resample = stbi__resample_row_h_2; + else if (r->hs == 2 && r->vs == 2) r->resample = z->resample_row_hv_2_kernel; + else r->resample = stbi__resample_row_generic; + } + + // can't error after this so, this is safe + output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); + if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + + // now go ahead and resample + for (j=0; j < z->s->img_y; ++j) { + stbi_uc *out = output + n * z->s->img_x * j; + for (k=0; k < decode_n; ++k) { + stbi__resample *r = &res_comp[k]; + int y_bot = r->ystep >= (r->vs >> 1); + coutput[k] = r->resample(z->img_comp[k].linebuf, + y_bot ? r->line1 : r->line0, + y_bot ? r->line0 : r->line1, + r->w_lores, r->hs); + if (++r->ystep >= r->vs) { + r->ystep = 0; + r->line0 = r->line1; + if (++r->ypos < z->img_comp[k].y) + r->line1 += z->img_comp[k].w2; + } + } + if (n >= 3) { + stbi_uc *y = coutput[0]; + if (z->s->img_n == 3) { + if (is_rgb) { + for (i=0; i < z->s->img_x; ++i) { + out[0] = y[i]; + out[1] = coutput[1][i]; + out[2] = coutput[2][i]; + out[3] = 255; + out += n; + } + } else { + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + } + } else if (z->s->img_n == 4) { + if (z->app14_color_transform == 0) { // CMYK + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(coutput[0][i], m); + out[1] = stbi__blinn_8x8(coutput[1][i], m); + out[2] = stbi__blinn_8x8(coutput[2][i], m); + out[3] = 255; + out += n; + } + } else if (z->app14_color_transform == 2) { // YCCK + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + out[0] = stbi__blinn_8x8(255 - out[0], m); + out[1] = stbi__blinn_8x8(255 - out[1], m); + out[2] = stbi__blinn_8x8(255 - out[2], m); + out += n; + } + } else { // YCbCr + alpha? Ignore the fourth channel for now + z->YCbCr_to_RGB_kernel(out, y, coutput[1], coutput[2], z->s->img_x, n); + } + } else + for (i=0; i < z->s->img_x; ++i) { + out[0] = out[1] = out[2] = y[i]; + out[3] = 255; // not used if n==3 + out += n; + } + } else { + if (is_rgb) { + if (n == 1) + for (i=0; i < z->s->img_x; ++i) + *out++ = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); + else { + for (i=0; i < z->s->img_x; ++i, out += 2) { + out[0] = stbi__compute_y(coutput[0][i], coutput[1][i], coutput[2][i]); + out[1] = 255; + } + } + } else if (z->s->img_n == 4 && z->app14_color_transform == 0) { + for (i=0; i < z->s->img_x; ++i) { + stbi_uc m = coutput[3][i]; + stbi_uc r = stbi__blinn_8x8(coutput[0][i], m); + stbi_uc g = stbi__blinn_8x8(coutput[1][i], m); + stbi_uc b = stbi__blinn_8x8(coutput[2][i], m); + out[0] = stbi__compute_y(r, g, b); + out[1] = 255; + out += n; + } + } else if (z->s->img_n == 4 && z->app14_color_transform == 2) { + for (i=0; i < z->s->img_x; ++i) { + out[0] = stbi__blinn_8x8(255 - coutput[0][i], coutput[3][i]); + out[1] = 255; + out += n; + } + } else { + stbi_uc *y = coutput[0]; + if (n == 1) + for (i=0; i < z->s->img_x; ++i) out[i] = y[i]; + else + for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; } + } + } + } + stbi__cleanup_jpeg(z); + *out_x = z->s->img_x; + *out_y = z->s->img_y; + if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output + return output; + } +} + +static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + unsigned char* result; + stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__errpuc("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + STBI_NOTUSED(ri); + j->s = s; + stbi__setup_jpeg(j); + result = load_jpeg_image(j, x,y,comp,req_comp); + STBI_FREE(j); + return result; +} + +static int stbi__jpeg_test(stbi__context *s) +{ + int r; + stbi__jpeg* j = (stbi__jpeg*)stbi__malloc(sizeof(stbi__jpeg)); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + j->s = s; + stbi__setup_jpeg(j); + r = stbi__decode_jpeg_header(j, STBI__SCAN_type); + stbi__rewind(s); + STBI_FREE(j); + return r; +} + +static int stbi__jpeg_info_raw(stbi__jpeg *j, int *x, int *y, int *comp) +{ + if (!stbi__decode_jpeg_header(j, STBI__SCAN_header)) { + stbi__rewind( j->s ); + return 0; + } + if (x) *x = j->s->img_x; + if (y) *y = j->s->img_y; + if (comp) *comp = j->s->img_n >= 3 ? 3 : 1; + return 1; +} + +static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp) +{ + int result; + stbi__jpeg* j = (stbi__jpeg*) (stbi__malloc(sizeof(stbi__jpeg))); + if (!j) return stbi__err("outofmem", "Out of memory"); + memset(j, 0, sizeof(stbi__jpeg)); + j->s = s; + result = stbi__jpeg_info_raw(j, x, y, comp); + STBI_FREE(j); + return result; +} +#endif + +// public domain zlib decode v0.2 Sean Barrett 2006-11-18 +// simple implementation +// - all input must be provided in an upfront buffer +// - all output is written to a single output buffer (can malloc/realloc) +// performance +// - fast huffman + +#ifndef STBI_NO_ZLIB + +// fast-way is faster to check than jpeg huffman, but slow way is slower +#define STBI__ZFAST_BITS 9 // accelerate all cases in default tables +#define STBI__ZFAST_MASK ((1 << STBI__ZFAST_BITS) - 1) +#define STBI__ZNSYMS 288 // number of symbols in literal/length alphabet + +// zlib-style huffman encoding +// (jpegs packs from left, zlib from right, so can't share code) +typedef struct +{ + stbi__uint16 fast[1 << STBI__ZFAST_BITS]; + stbi__uint16 firstcode[16]; + int maxcode[17]; + stbi__uint16 firstsymbol[16]; + stbi_uc size[STBI__ZNSYMS]; + stbi__uint16 value[STBI__ZNSYMS]; +} stbi__zhuffman; + +stbi_inline static int stbi__bitreverse16(int n) +{ + n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); + n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); + n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); + n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); + return n; +} + +stbi_inline static int stbi__bit_reverse(int v, int bits) +{ + STBI_ASSERT(bits <= 16); + // to bit reverse n bits, reverse 16 and shift + // e.g. 11 bits, bit reverse and shift away 5 + return stbi__bitreverse16(v) >> (16-bits); +} + +static int stbi__zbuild_huffman(stbi__zhuffman *z, const stbi_uc *sizelist, int num) +{ + int i,k=0; + int code, next_code[16], sizes[17]; + + // DEFLATE spec for generating codes + memset(sizes, 0, sizeof(sizes)); + memset(z->fast, 0, sizeof(z->fast)); + for (i=0; i < num; ++i) + ++sizes[sizelist[i]]; + sizes[0] = 0; + for (i=1; i < 16; ++i) + if (sizes[i] > (1 << i)) + return stbi__err("bad sizes", "Corrupt PNG"); + code = 0; + for (i=1; i < 16; ++i) { + next_code[i] = code; + z->firstcode[i] = (stbi__uint16) code; + z->firstsymbol[i] = (stbi__uint16) k; + code = (code + sizes[i]); + if (sizes[i]) + if (code-1 >= (1 << i)) return stbi__err("bad codelengths","Corrupt PNG"); + z->maxcode[i] = code << (16-i); // preshift for inner loop + code <<= 1; + k += sizes[i]; + } + z->maxcode[16] = 0x10000; // sentinel + for (i=0; i < num; ++i) { + int s = sizelist[i]; + if (s) { + int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; + stbi__uint16 fastv = (stbi__uint16) ((s << 9) | i); + z->size [c] = (stbi_uc ) s; + z->value[c] = (stbi__uint16) i; + if (s <= STBI__ZFAST_BITS) { + int j = stbi__bit_reverse(next_code[s],s); + while (j < (1 << STBI__ZFAST_BITS)) { + z->fast[j] = fastv; + j += (1 << s); + } + } + ++next_code[s]; + } + } + return 1; +} + +// zlib-from-memory implementation for PNG reading +// because PNG allows splitting the zlib stream arbitrarily, +// and it's annoying structurally to have PNG call ZLIB call PNG, +// we require PNG read all the IDATs and combine them into a single +// memory buffer + +typedef struct +{ + stbi_uc *zbuffer, *zbuffer_end; + int num_bits; + int hit_zeof_once; + stbi__uint32 code_buffer; + + char *zout; + char *zout_start; + char *zout_end; + int z_expandable; + + stbi__zhuffman z_length, z_distance; +} stbi__zbuf; + +stbi_inline static int stbi__zeof(stbi__zbuf *z) +{ + return (z->zbuffer >= z->zbuffer_end); +} + +stbi_inline static stbi_uc stbi__zget8(stbi__zbuf *z) +{ + return stbi__zeof(z) ? 0 : *z->zbuffer++; +} + +static void stbi__fill_bits(stbi__zbuf *z) +{ + do { + if (z->code_buffer >= (1U << z->num_bits)) { + z->zbuffer = z->zbuffer_end; /* treat this as EOF so we fail. */ + return; + } + z->code_buffer |= (unsigned int) stbi__zget8(z) << z->num_bits; + z->num_bits += 8; + } while (z->num_bits <= 24); +} + +stbi_inline static unsigned int stbi__zreceive(stbi__zbuf *z, int n) +{ + unsigned int k; + if (z->num_bits < n) stbi__fill_bits(z); + k = z->code_buffer & ((1 << n) - 1); + z->code_buffer >>= n; + z->num_bits -= n; + return k; +} + +static int stbi__zhuffman_decode_slowpath(stbi__zbuf *a, stbi__zhuffman *z) +{ + int b,s,k; + // not resolved by fast table, so compute it the slow way + // use jpeg approach, which requires MSbits at top + k = stbi__bit_reverse(a->code_buffer, 16); + for (s=STBI__ZFAST_BITS+1; ; ++s) + if (k < z->maxcode[s]) + break; + if (s >= 16) return -1; // invalid code! + // code size is s, so: + b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; + if (b >= STBI__ZNSYMS) return -1; // some data was corrupt somewhere! + if (z->size[b] != s) return -1; // was originally an assert, but report failure instead. + a->code_buffer >>= s; + a->num_bits -= s; + return z->value[b]; +} + +stbi_inline static int stbi__zhuffman_decode(stbi__zbuf *a, stbi__zhuffman *z) +{ + int b,s; + if (a->num_bits < 16) { + if (stbi__zeof(a)) { + if (!a->hit_zeof_once) { + // This is the first time we hit eof, insert 16 extra padding btis + // to allow us to keep going; if we actually consume any of them + // though, that is invalid data. This is caught later. + a->hit_zeof_once = 1; + a->num_bits += 16; // add 16 implicit zero bits + } else { + // We already inserted our extra 16 padding bits and are again + // out, this stream is actually prematurely terminated. + return -1; + } + } else { + stbi__fill_bits(a); + } + } + b = z->fast[a->code_buffer & STBI__ZFAST_MASK]; + if (b) { + s = b >> 9; + a->code_buffer >>= s; + a->num_bits -= s; + return b & 511; + } + return stbi__zhuffman_decode_slowpath(a, z); +} + +static int stbi__zexpand(stbi__zbuf *z, char *zout, int n) // need to make room for n bytes +{ + char *q; + unsigned int cur, limit, old_limit; + z->zout = zout; + if (!z->z_expandable) return stbi__err("output buffer limit","Corrupt PNG"); + cur = (unsigned int) (z->zout - z->zout_start); + limit = old_limit = (unsigned) (z->zout_end - z->zout_start); + if (UINT_MAX - cur < (unsigned) n) return stbi__err("outofmem", "Out of memory"); + while (cur + n > limit) { + if(limit > UINT_MAX / 2) return stbi__err("outofmem", "Out of memory"); + limit *= 2; + } + q = (char *) STBI_REALLOC_SIZED(z->zout_start, old_limit, limit); + STBI_NOTUSED(old_limit); + if (q == NULL) return stbi__err("outofmem", "Out of memory"); + z->zout_start = q; + z->zout = q + cur; + z->zout_end = q + limit; + return 1; +} + +static const int stbi__zlength_base[31] = { + 3,4,5,6,7,8,9,10,11,13, + 15,17,19,23,27,31,35,43,51,59, + 67,83,99,115,131,163,195,227,258,0,0 }; + +static const int stbi__zlength_extra[31]= +{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; + +static const int stbi__zdist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, +257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; + +static const int stbi__zdist_extra[32] = +{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; + +static int stbi__parse_huffman_block(stbi__zbuf *a) +{ + char *zout = a->zout; + for(;;) { + int z = stbi__zhuffman_decode(a, &a->z_length); + if (z < 256) { + if (z < 0) return stbi__err("bad huffman code","Corrupt PNG"); // error in huffman codes + if (zout >= a->zout_end) { + if (!stbi__zexpand(a, zout, 1)) return 0; + zout = a->zout; + } + *zout++ = (char) z; + } else { + stbi_uc *p; + int len,dist; + if (z == 256) { + a->zout = zout; + if (a->hit_zeof_once && a->num_bits < 16) { + // The first time we hit zeof, we inserted 16 extra zero bits into our bit + // buffer so the decoder can just do its speculative decoding. But if we + // actually consumed any of those bits (which is the case when num_bits < 16), + // the stream actually read past the end so it is malformed. + return stbi__err("unexpected end","Corrupt PNG"); + } + return 1; + } + if (z >= 286) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, length codes 286 and 287 must not appear in compressed data + z -= 257; + len = stbi__zlength_base[z]; + if (stbi__zlength_extra[z]) len += stbi__zreceive(a, stbi__zlength_extra[z]); + z = stbi__zhuffman_decode(a, &a->z_distance); + if (z < 0 || z >= 30) return stbi__err("bad huffman code","Corrupt PNG"); // per DEFLATE, distance codes 30 and 31 must not appear in compressed data + dist = stbi__zdist_base[z]; + if (stbi__zdist_extra[z]) dist += stbi__zreceive(a, stbi__zdist_extra[z]); + if (zout - a->zout_start < dist) return stbi__err("bad dist","Corrupt PNG"); + if (len > a->zout_end - zout) { + if (!stbi__zexpand(a, zout, len)) return 0; + zout = a->zout; + } + p = (stbi_uc *) (zout - dist); + if (dist == 1) { // run of one byte; common in images. + stbi_uc v = *p; + if (len) { do *zout++ = v; while (--len); } + } else { + if (len) { do *zout++ = *p++; while (--len); } + } + } + } +} + +static int stbi__compute_huffman_codes(stbi__zbuf *a) +{ + static const stbi_uc length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + stbi__zhuffman z_codelength; + stbi_uc lencodes[286+32+137];//padding for maximum single op + stbi_uc codelength_sizes[19]; + int i,n; + + int hlit = stbi__zreceive(a,5) + 257; + int hdist = stbi__zreceive(a,5) + 1; + int hclen = stbi__zreceive(a,4) + 4; + int ntot = hlit + hdist; + + memset(codelength_sizes, 0, sizeof(codelength_sizes)); + for (i=0; i < hclen; ++i) { + int s = stbi__zreceive(a,3); + codelength_sizes[length_dezigzag[i]] = (stbi_uc) s; + } + if (!stbi__zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; + + n = 0; + while (n < ntot) { + int c = stbi__zhuffman_decode(a, &z_codelength); + if (c < 0 || c >= 19) return stbi__err("bad codelengths", "Corrupt PNG"); + if (c < 16) + lencodes[n++] = (stbi_uc) c; + else { + stbi_uc fill = 0; + if (c == 16) { + c = stbi__zreceive(a,2)+3; + if (n == 0) return stbi__err("bad codelengths", "Corrupt PNG"); + fill = lencodes[n-1]; + } else if (c == 17) { + c = stbi__zreceive(a,3)+3; + } else if (c == 18) { + c = stbi__zreceive(a,7)+11; + } else { + return stbi__err("bad codelengths", "Corrupt PNG"); + } + if (ntot - n < c) return stbi__err("bad codelengths", "Corrupt PNG"); + memset(lencodes+n, fill, c); + n += c; + } + } + if (n != ntot) return stbi__err("bad codelengths","Corrupt PNG"); + if (!stbi__zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; + if (!stbi__zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; + return 1; +} + +static int stbi__parse_uncompressed_block(stbi__zbuf *a) +{ + stbi_uc header[4]; + int len,nlen,k; + if (a->num_bits & 7) + stbi__zreceive(a, a->num_bits & 7); // discard + // drain the bit-packed data into header + k = 0; + while (a->num_bits > 0) { + header[k++] = (stbi_uc) (a->code_buffer & 255); // suppress MSVC run-time check + a->code_buffer >>= 8; + a->num_bits -= 8; + } + if (a->num_bits < 0) return stbi__err("zlib corrupt","Corrupt PNG"); + // now fill header the normal way + while (k < 4) + header[k++] = stbi__zget8(a); + len = header[1] * 256 + header[0]; + nlen = header[3] * 256 + header[2]; + if (nlen != (len ^ 0xffff)) return stbi__err("zlib corrupt","Corrupt PNG"); + if (a->zbuffer + len > a->zbuffer_end) return stbi__err("read past buffer","Corrupt PNG"); + if (a->zout + len > a->zout_end) + if (!stbi__zexpand(a, a->zout, len)) return 0; + memcpy(a->zout, a->zbuffer, len); + a->zbuffer += len; + a->zout += len; + return 1; +} + +static int stbi__parse_zlib_header(stbi__zbuf *a) +{ + int cmf = stbi__zget8(a); + int cm = cmf & 15; + /* int cinfo = cmf >> 4; */ + int flg = stbi__zget8(a); + if (stbi__zeof(a)) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec + if ((cmf*256+flg) % 31 != 0) return stbi__err("bad zlib header","Corrupt PNG"); // zlib spec + if (flg & 32) return stbi__err("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png + if (cm != 8) return stbi__err("bad compression","Corrupt PNG"); // DEFLATE required for png + // window = 1 << (8 + cinfo)... but who cares, we fully buffer output + return 1; +} + +static const stbi_uc stbi__zdefault_length[STBI__ZNSYMS] = +{ + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, + 8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8 +}; +static const stbi_uc stbi__zdefault_distance[32] = +{ + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5 +}; +/* +Init algorithm: +{ + int i; // use <= to match clearly with spec + for (i=0; i <= 143; ++i) stbi__zdefault_length[i] = 8; + for ( ; i <= 255; ++i) stbi__zdefault_length[i] = 9; + for ( ; i <= 279; ++i) stbi__zdefault_length[i] = 7; + for ( ; i <= 287; ++i) stbi__zdefault_length[i] = 8; + + for (i=0; i <= 31; ++i) stbi__zdefault_distance[i] = 5; +} +*/ + +static int stbi__parse_zlib(stbi__zbuf *a, int parse_header) +{ + int final, type; + if (parse_header) + if (!stbi__parse_zlib_header(a)) return 0; + a->num_bits = 0; + a->code_buffer = 0; + a->hit_zeof_once = 0; + do { + final = stbi__zreceive(a,1); + type = stbi__zreceive(a,2); + if (type == 0) { + if (!stbi__parse_uncompressed_block(a)) return 0; + } else if (type == 3) { + return 0; + } else { + if (type == 1) { + // use fixed code lengths + if (!stbi__zbuild_huffman(&a->z_length , stbi__zdefault_length , STBI__ZNSYMS)) return 0; + if (!stbi__zbuild_huffman(&a->z_distance, stbi__zdefault_distance, 32)) return 0; + } else { + if (!stbi__compute_huffman_codes(a)) return 0; + } + if (!stbi__parse_huffman_block(a)) return 0; + } + } while (!final); + return 1; +} + +static int stbi__do_zlib(stbi__zbuf *a, char *obuf, int olen, int exp, int parse_header) +{ + a->zout_start = obuf; + a->zout = obuf; + a->zout_end = obuf + olen; + a->z_expandable = exp; + + return stbi__parse_zlib(a, parse_header); +} + +STBIDEF char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, 1)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen) +{ + return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); +} + +STBIDEF char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer + len; + if (stbi__do_zlib(&a, p, initial_size, 1, parse_header)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc *) ibuffer; + a.zbuffer_end = (stbi_uc *) ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 1)) + return (int) (a.zout - a.zout_start); + else + return -1; +} + +STBIDEF char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen) +{ + stbi__zbuf a; + char *p = (char *) stbi__malloc(16384); + if (p == NULL) return NULL; + a.zbuffer = (stbi_uc *) buffer; + a.zbuffer_end = (stbi_uc *) buffer+len; + if (stbi__do_zlib(&a, p, 16384, 1, 0)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + STBI_FREE(a.zout_start); + return NULL; + } +} + +STBIDEF int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) +{ + stbi__zbuf a; + a.zbuffer = (stbi_uc *) ibuffer; + a.zbuffer_end = (stbi_uc *) ibuffer + ilen; + if (stbi__do_zlib(&a, obuffer, olen, 0, 0)) + return (int) (a.zout - a.zout_start); + else + return -1; +} +#endif + +// public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 +// simple implementation +// - only 8-bit samples +// - no CRC checking +// - allocates lots of intermediate memory +// - avoids problem of streaming data between subsystems +// - avoids explicit window management +// performance +// - uses stb_zlib, a PD zlib implementation with fast huffman decoding + +#ifndef STBI_NO_PNG +typedef struct +{ + stbi__uint32 length; + stbi__uint32 type; +} stbi__pngchunk; + +static stbi__pngchunk stbi__get_chunk_header(stbi__context *s) +{ + stbi__pngchunk c; + c.length = stbi__get32be(s); + c.type = stbi__get32be(s); + return c; +} + +static int stbi__check_png_header(stbi__context *s) +{ + static const stbi_uc png_sig[8] = { 137,80,78,71,13,10,26,10 }; + int i; + for (i=0; i < 8; ++i) + if (stbi__get8(s) != png_sig[i]) return stbi__err("bad png sig","Not a PNG"); + return 1; +} + +typedef struct +{ + stbi__context *s; + stbi_uc *idata, *expanded, *out; + int depth; +} stbi__png; + + +enum { + STBI__F_none=0, + STBI__F_sub=1, + STBI__F_up=2, + STBI__F_avg=3, + STBI__F_paeth=4, + // synthetic filter used for first scanline to avoid needing a dummy row of 0s + STBI__F_avg_first +}; + +static stbi_uc first_row_filter[5] = +{ + STBI__F_none, + STBI__F_sub, + STBI__F_none, + STBI__F_avg_first, + STBI__F_sub // Paeth with b=c=0 turns out to be equivalent to sub +}; + +static int stbi__paeth(int a, int b, int c) +{ + // This formulation looks very different from the reference in the PNG spec, but is + // actually equivalent and has favorable data dependencies and admits straightforward + // generation of branch-free code, which helps performance significantly. + int thresh = c*3 - (a + b); + int lo = a < b ? a : b; + int hi = a < b ? b : a; + int t0 = (hi <= thresh) ? lo : c; + int t1 = (thresh <= lo) ? hi : t0; + return t1; +} + +static const stbi_uc stbi__depth_scale_table[9] = { 0, 0xff, 0x55, 0, 0x11, 0,0,0, 0x01 }; + +// adds an extra all-255 alpha channel +// dest == src is legal +// img_n must be 1 or 3 +static void stbi__create_png_alpha_expand8(stbi_uc *dest, stbi_uc *src, stbi__uint32 x, int img_n) +{ + int i; + // must process data backwards since we allow dest==src + if (img_n == 1) { + for (i=x-1; i >= 0; --i) { + dest[i*2+1] = 255; + dest[i*2+0] = src[i]; + } + } else { + STBI_ASSERT(img_n == 3); + for (i=x-1; i >= 0; --i) { + dest[i*4+3] = 255; + dest[i*4+2] = src[i*3+2]; + dest[i*4+1] = src[i*3+1]; + dest[i*4+0] = src[i*3+0]; + } + } +} + +// create the png data from post-deflated data +static int stbi__create_png_image_raw(stbi__png *a, stbi_uc *raw, stbi__uint32 raw_len, int out_n, stbi__uint32 x, stbi__uint32 y, int depth, int color) +{ + int bytes = (depth == 16 ? 2 : 1); + stbi__context *s = a->s; + stbi__uint32 i,j,stride = x*out_n*bytes; + stbi__uint32 img_len, img_width_bytes; + stbi_uc *filter_buf; + int all_ok = 1; + int k; + int img_n = s->img_n; // copy it into a local for later + + int output_bytes = out_n*bytes; + int filter_bytes = img_n*bytes; + int width = x; + + STBI_ASSERT(out_n == s->img_n || out_n == s->img_n+1); + a->out = (stbi_uc *) stbi__malloc_mad3(x, y, output_bytes, 0); // extra bytes to write off the end into + if (!a->out) return stbi__err("outofmem", "Out of memory"); + + // note: error exits here don't need to clean up a->out individually, + // stbi__do_png always does on error. + if (!stbi__mad3sizes_valid(img_n, x, depth, 7)) return stbi__err("too large", "Corrupt PNG"); + img_width_bytes = (((img_n * x * depth) + 7) >> 3); + if (!stbi__mad2sizes_valid(img_width_bytes, y, img_width_bytes)) return stbi__err("too large", "Corrupt PNG"); + img_len = (img_width_bytes + 1) * y; + + // we used to check for exact match between raw_len and img_len on non-interlaced PNGs, + // but issue #276 reported a PNG in the wild that had extra data at the end (all zeros), + // so just check for raw_len < img_len always. + if (raw_len < img_len) return stbi__err("not enough pixels","Corrupt PNG"); + + // Allocate two scan lines worth of filter workspace buffer. + filter_buf = (stbi_uc *) stbi__malloc_mad2(img_width_bytes, 2, 0); + if (!filter_buf) return stbi__err("outofmem", "Out of memory"); + + // Filtering for low-bit-depth images + if (depth < 8) { + filter_bytes = 1; + width = img_width_bytes; + } + + for (j=0; j < y; ++j) { + // cur/prior filter buffers alternate + stbi_uc *cur = filter_buf + (j & 1)*img_width_bytes; + stbi_uc *prior = filter_buf + (~j & 1)*img_width_bytes; + stbi_uc *dest = a->out + stride*j; + int nk = width * filter_bytes; + int filter = *raw++; + + // check filter type + if (filter > 4) { + all_ok = stbi__err("invalid filter","Corrupt PNG"); + break; + } + + // if first row, use special filter that doesn't sample previous row + if (j == 0) filter = first_row_filter[filter]; + + // perform actual filtering + switch (filter) { + case STBI__F_none: + memcpy(cur, raw, nk); + break; + case STBI__F_sub: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + cur[k-filter_bytes]); + break; + case STBI__F_up: + for (k = 0; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); + break; + case STBI__F_avg: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (prior[k]>>1)); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + ((prior[k] + cur[k-filter_bytes])>>1)); + break; + case STBI__F_paeth: + for (k = 0; k < filter_bytes; ++k) + cur[k] = STBI__BYTECAST(raw[k] + prior[k]); // prior[k] == stbi__paeth(0,prior[k],0) + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + stbi__paeth(cur[k-filter_bytes], prior[k], prior[k-filter_bytes])); + break; + case STBI__F_avg_first: + memcpy(cur, raw, filter_bytes); + for (k = filter_bytes; k < nk; ++k) + cur[k] = STBI__BYTECAST(raw[k] + (cur[k-filter_bytes] >> 1)); + break; + } + + raw += nk; + + // expand decoded bits in cur to dest, also adding an extra alpha channel if desired + if (depth < 8) { + stbi_uc scale = (color == 0) ? stbi__depth_scale_table[depth] : 1; // scale grayscale values to 0..255 range + stbi_uc *in = cur; + stbi_uc *out = dest; + stbi_uc inb = 0; + stbi__uint32 nsmp = x*img_n; + + // expand bits to bytes first + if (depth == 4) { + for (i=0; i < nsmp; ++i) { + if ((i & 1) == 0) inb = *in++; + *out++ = scale * (inb >> 4); + inb <<= 4; + } + } else if (depth == 2) { + for (i=0; i < nsmp; ++i) { + if ((i & 3) == 0) inb = *in++; + *out++ = scale * (inb >> 6); + inb <<= 2; + } + } else { + STBI_ASSERT(depth == 1); + for (i=0; i < nsmp; ++i) { + if ((i & 7) == 0) inb = *in++; + *out++ = scale * (inb >> 7); + inb <<= 1; + } + } + + // insert alpha=255 values if desired + if (img_n != out_n) + stbi__create_png_alpha_expand8(dest, dest, x, img_n); + } else if (depth == 8) { + if (img_n == out_n) + memcpy(dest, cur, x*img_n); + else + stbi__create_png_alpha_expand8(dest, cur, x, img_n); + } else if (depth == 16) { + // convert the image data from big-endian to platform-native + stbi__uint16 *dest16 = (stbi__uint16*)dest; + stbi__uint32 nsmp = x*img_n; + + if (img_n == out_n) { + for (i = 0; i < nsmp; ++i, ++dest16, cur += 2) + *dest16 = (cur[0] << 8) | cur[1]; + } else { + STBI_ASSERT(img_n+1 == out_n); + if (img_n == 1) { + for (i = 0; i < x; ++i, dest16 += 2, cur += 2) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = 0xffff; + } + } else { + STBI_ASSERT(img_n == 3); + for (i = 0; i < x; ++i, dest16 += 4, cur += 6) { + dest16[0] = (cur[0] << 8) | cur[1]; + dest16[1] = (cur[2] << 8) | cur[3]; + dest16[2] = (cur[4] << 8) | cur[5]; + dest16[3] = 0xffff; + } + } + } + } + } + + STBI_FREE(filter_buf); + if (!all_ok) return 0; + + return 1; +} + +static int stbi__create_png_image(stbi__png *a, stbi_uc *image_data, stbi__uint32 image_data_len, int out_n, int depth, int color, int interlaced) +{ + int bytes = (depth == 16 ? 2 : 1); + int out_bytes = out_n * bytes; + stbi_uc *final; + int p; + if (!interlaced) + return stbi__create_png_image_raw(a, image_data, image_data_len, out_n, a->s->img_x, a->s->img_y, depth, color); + + // de-interlacing + final = (stbi_uc *) stbi__malloc_mad3(a->s->img_x, a->s->img_y, out_bytes, 0); + if (!final) return stbi__err("outofmem", "Out of memory"); + for (p=0; p < 7; ++p) { + int xorig[] = { 0,4,0,2,0,1,0 }; + int yorig[] = { 0,0,4,0,2,0,1 }; + int xspc[] = { 8,8,4,4,2,2,1 }; + int yspc[] = { 8,8,8,4,4,2,2 }; + int i,j,x,y; + // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 + x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p]; + y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p]; + if (x && y) { + stbi__uint32 img_len = ((((a->s->img_n * x * depth) + 7) >> 3) + 1) * y; + if (!stbi__create_png_image_raw(a, image_data, image_data_len, out_n, x, y, depth, color)) { + STBI_FREE(final); + return 0; + } + for (j=0; j < y; ++j) { + for (i=0; i < x; ++i) { + int out_y = j*yspc[p]+yorig[p]; + int out_x = i*xspc[p]+xorig[p]; + memcpy(final + out_y*a->s->img_x*out_bytes + out_x*out_bytes, + a->out + (j*x+i)*out_bytes, out_bytes); + } + } + STBI_FREE(a->out); + image_data += img_len; + image_data_len -= img_len; + } + } + a->out = final; + + return 1; +} + +static int stbi__compute_transparency(stbi__png *z, stbi_uc tc[3], int out_n) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc *p = z->out; + + // compute color-based transparency, assuming we've + // already got 255 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i=0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 255); + p += 2; + } + } else { + for (i=0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__compute_transparency16(stbi__png *z, stbi__uint16 tc[3], int out_n) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi__uint16 *p = (stbi__uint16*) z->out; + + // compute color-based transparency, assuming we've + // already got 65535 as the alpha value in the output + STBI_ASSERT(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i = 0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 65535); + p += 2; + } + } else { + for (i = 0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int stbi__expand_png_palette(stbi__png *a, stbi_uc *palette, int len, int pal_img_n) +{ + stbi__uint32 i, pixel_count = a->s->img_x * a->s->img_y; + stbi_uc *p, *temp_out, *orig = a->out; + + p = (stbi_uc *) stbi__malloc_mad2(pixel_count, pal_img_n, 0); + if (p == NULL) return stbi__err("outofmem", "Out of memory"); + + // between here and free(out) below, exitting would leak + temp_out = p; + + if (pal_img_n == 3) { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p += 3; + } + } else { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p[3] = palette[n+3]; + p += 4; + } + } + STBI_FREE(a->out); + a->out = temp_out; + + STBI_NOTUSED(len); + + return 1; +} + +static int stbi__unpremultiply_on_load_global = 0; +static int stbi__de_iphone_flag_global = 0; + +STBIDEF void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_global = flag_true_if_should_unpremultiply; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_global = flag_true_if_should_convert; +} + +#ifndef STBI_THREAD_LOCAL +#define stbi__unpremultiply_on_load stbi__unpremultiply_on_load_global +#define stbi__de_iphone_flag stbi__de_iphone_flag_global +#else +static STBI_THREAD_LOCAL int stbi__unpremultiply_on_load_local, stbi__unpremultiply_on_load_set; +static STBI_THREAD_LOCAL int stbi__de_iphone_flag_local, stbi__de_iphone_flag_set; + +STBIDEF void stbi_set_unpremultiply_on_load_thread(int flag_true_if_should_unpremultiply) +{ + stbi__unpremultiply_on_load_local = flag_true_if_should_unpremultiply; + stbi__unpremultiply_on_load_set = 1; +} + +STBIDEF void stbi_convert_iphone_png_to_rgb_thread(int flag_true_if_should_convert) +{ + stbi__de_iphone_flag_local = flag_true_if_should_convert; + stbi__de_iphone_flag_set = 1; +} + +#define stbi__unpremultiply_on_load (stbi__unpremultiply_on_load_set \ + ? stbi__unpremultiply_on_load_local \ + : stbi__unpremultiply_on_load_global) +#define stbi__de_iphone_flag (stbi__de_iphone_flag_set \ + ? stbi__de_iphone_flag_local \ + : stbi__de_iphone_flag_global) +#endif // STBI_THREAD_LOCAL + +static void stbi__de_iphone(stbi__png *z) +{ + stbi__context *s = z->s; + stbi__uint32 i, pixel_count = s->img_x * s->img_y; + stbi_uc *p = z->out; + + if (s->img_out_n == 3) { // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 3; + } + } else { + STBI_ASSERT(s->img_out_n == 4); + if (stbi__unpremultiply_on_load) { + // convert bgr to rgb and unpremultiply + for (i=0; i < pixel_count; ++i) { + stbi_uc a = p[3]; + stbi_uc t = p[0]; + if (a) { + stbi_uc half = a / 2; + p[0] = (p[2] * 255 + half) / a; + p[1] = (p[1] * 255 + half) / a; + p[2] = ( t * 255 + half) / a; + } else { + p[0] = p[2]; + p[2] = t; + } + p += 4; + } + } else { + // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + stbi_uc t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 4; + } + } + } +} + +#define STBI__PNG_TYPE(a,b,c,d) (((unsigned) (a) << 24) + ((unsigned) (b) << 16) + ((unsigned) (c) << 8) + (unsigned) (d)) + +static int stbi__parse_png_file(stbi__png *z, int scan, int req_comp) +{ + stbi_uc palette[1024], pal_img_n=0; + stbi_uc has_trans=0, tc[3]={0}; + stbi__uint16 tc16[3]; + stbi__uint32 ioff=0, idata_limit=0, i, pal_len=0; + int first=1,k,interlace=0, color=0, is_iphone=0; + stbi__context *s = z->s; + + z->expanded = NULL; + z->idata = NULL; + z->out = NULL; + + if (!stbi__check_png_header(s)) return 0; + + if (scan == STBI__SCAN_type) return 1; + + for (;;) { + stbi__pngchunk c = stbi__get_chunk_header(s); + switch (c.type) { + case STBI__PNG_TYPE('C','g','B','I'): + is_iphone = 1; + stbi__skip(s, c.length); + break; + case STBI__PNG_TYPE('I','H','D','R'): { + int comp,filter; + if (!first) return stbi__err("multiple IHDR","Corrupt PNG"); + first = 0; + if (c.length != 13) return stbi__err("bad IHDR len","Corrupt PNG"); + s->img_x = stbi__get32be(s); + s->img_y = stbi__get32be(s); + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + z->depth = stbi__get8(s); if (z->depth != 1 && z->depth != 2 && z->depth != 4 && z->depth != 8 && z->depth != 16) return stbi__err("1/2/4/8/16-bit only","PNG not supported: 1/2/4/8/16-bit only"); + color = stbi__get8(s); if (color > 6) return stbi__err("bad ctype","Corrupt PNG"); + if (color == 3 && z->depth == 16) return stbi__err("bad ctype","Corrupt PNG"); + if (color == 3) pal_img_n = 3; else if (color & 1) return stbi__err("bad ctype","Corrupt PNG"); + comp = stbi__get8(s); if (comp) return stbi__err("bad comp method","Corrupt PNG"); + filter= stbi__get8(s); if (filter) return stbi__err("bad filter method","Corrupt PNG"); + interlace = stbi__get8(s); if (interlace>1) return stbi__err("bad interlace method","Corrupt PNG"); + if (!s->img_x || !s->img_y) return stbi__err("0-pixel image","Corrupt PNG"); + if (!pal_img_n) { + s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); + if ((1 << 30) / s->img_x / s->img_n < s->img_y) return stbi__err("too large", "Image too large to decode"); + } else { + // if paletted, then pal_n is our final components, and + // img_n is # components to decompress/filter. + s->img_n = 1; + if ((1 << 30) / s->img_x / 4 < s->img_y) return stbi__err("too large","Corrupt PNG"); + } + // even with SCAN_header, have to scan to see if we have a tRNS + break; + } + + case STBI__PNG_TYPE('P','L','T','E'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (c.length > 256*3) return stbi__err("invalid PLTE","Corrupt PNG"); + pal_len = c.length / 3; + if (pal_len * 3 != c.length) return stbi__err("invalid PLTE","Corrupt PNG"); + for (i=0; i < pal_len; ++i) { + palette[i*4+0] = stbi__get8(s); + palette[i*4+1] = stbi__get8(s); + palette[i*4+2] = stbi__get8(s); + palette[i*4+3] = 255; + } + break; + } + + case STBI__PNG_TYPE('t','R','N','S'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (z->idata) return stbi__err("tRNS after IDAT","Corrupt PNG"); + if (pal_img_n) { + if (scan == STBI__SCAN_header) { s->img_n = 4; return 1; } + if (pal_len == 0) return stbi__err("tRNS before PLTE","Corrupt PNG"); + if (c.length > pal_len) return stbi__err("bad tRNS len","Corrupt PNG"); + pal_img_n = 4; + for (i=0; i < c.length; ++i) + palette[i*4+3] = stbi__get8(s); + } else { + if (!(s->img_n & 1)) return stbi__err("tRNS with alpha","Corrupt PNG"); + if (c.length != (stbi__uint32) s->img_n*2) return stbi__err("bad tRNS len","Corrupt PNG"); + has_trans = 1; + // non-paletted with tRNS = constant alpha. if header-scanning, we can stop now. + if (scan == STBI__SCAN_header) { ++s->img_n; return 1; } + if (z->depth == 16) { + for (k = 0; k < s->img_n && k < 3; ++k) // extra loop test to suppress false GCC warning + tc16[k] = (stbi__uint16)stbi__get16be(s); // copy the values as-is + } else { + for (k = 0; k < s->img_n && k < 3; ++k) + tc[k] = (stbi_uc)(stbi__get16be(s) & 255) * stbi__depth_scale_table[z->depth]; // non 8-bit images will be larger + } + } + break; + } + + case STBI__PNG_TYPE('I','D','A','T'): { + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (pal_img_n && !pal_len) return stbi__err("no PLTE","Corrupt PNG"); + if (scan == STBI__SCAN_header) { + // header scan definitely stops at first IDAT + if (pal_img_n) + s->img_n = pal_img_n; + return 1; + } + if (c.length > (1u << 30)) return stbi__err("IDAT size limit", "IDAT section larger than 2^30 bytes"); + if ((int)(ioff + c.length) < (int)ioff) return 0; + if (ioff + c.length > idata_limit) { + stbi__uint32 idata_limit_old = idata_limit; + stbi_uc *p; + if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; + while (ioff + c.length > idata_limit) + idata_limit *= 2; + STBI_NOTUSED(idata_limit_old); + p = (stbi_uc *) STBI_REALLOC_SIZED(z->idata, idata_limit_old, idata_limit); if (p == NULL) return stbi__err("outofmem", "Out of memory"); + z->idata = p; + } + if (!stbi__getn(s, z->idata+ioff,c.length)) return stbi__err("outofdata","Corrupt PNG"); + ioff += c.length; + break; + } + + case STBI__PNG_TYPE('I','E','N','D'): { + stbi__uint32 raw_len, bpl; + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if (scan != STBI__SCAN_load) return 1; + if (z->idata == NULL) return stbi__err("no IDAT","Corrupt PNG"); + // initial guess for decoded data size to avoid unnecessary reallocs + bpl = (s->img_x * z->depth + 7) / 8; // bytes per line, per component + raw_len = bpl * s->img_y * s->img_n /* pixels */ + s->img_y /* filter mode per row */; + z->expanded = (stbi_uc *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, raw_len, (int *) &raw_len, !is_iphone); + if (z->expanded == NULL) return 0; // zlib should set error + STBI_FREE(z->idata); z->idata = NULL; + if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) + s->img_out_n = s->img_n+1; + else + s->img_out_n = s->img_n; + if (!stbi__create_png_image(z, z->expanded, raw_len, s->img_out_n, z->depth, color, interlace)) return 0; + if (has_trans) { + if (z->depth == 16) { + if (!stbi__compute_transparency16(z, tc16, s->img_out_n)) return 0; + } else { + if (!stbi__compute_transparency(z, tc, s->img_out_n)) return 0; + } + } + if (is_iphone && stbi__de_iphone_flag && s->img_out_n > 2) + stbi__de_iphone(z); + if (pal_img_n) { + // pal_img_n == 3 or 4 + s->img_n = pal_img_n; // record the actual colors we had + s->img_out_n = pal_img_n; + if (req_comp >= 3) s->img_out_n = req_comp; + if (!stbi__expand_png_palette(z, palette, pal_len, s->img_out_n)) + return 0; + } else if (has_trans) { + // non-paletted image with tRNS -> source image has (constant) alpha + ++s->img_n; + } + STBI_FREE(z->expanded); z->expanded = NULL; + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + return 1; + } + + default: + // if critical, fail + if (first) return stbi__err("first not IHDR", "Corrupt PNG"); + if ((c.type & (1 << 29)) == 0) { + #ifndef STBI_NO_FAILURE_STRINGS + // not threadsafe + static char invalid_chunk[] = "XXXX PNG chunk not known"; + invalid_chunk[0] = STBI__BYTECAST(c.type >> 24); + invalid_chunk[1] = STBI__BYTECAST(c.type >> 16); + invalid_chunk[2] = STBI__BYTECAST(c.type >> 8); + invalid_chunk[3] = STBI__BYTECAST(c.type >> 0); + #endif + return stbi__err(invalid_chunk, "PNG not supported: unknown PNG chunk type"); + } + stbi__skip(s, c.length); + break; + } + // end of PNG chunk, read and skip CRC + stbi__get32be(s); + } +} + +static void *stbi__do_png(stbi__png *p, int *x, int *y, int *n, int req_comp, stbi__result_info *ri) +{ + void *result=NULL; + if (req_comp < 0 || req_comp > 4) return stbi__errpuc("bad req_comp", "Internal error"); + if (stbi__parse_png_file(p, STBI__SCAN_load, req_comp)) { + if (p->depth <= 8) + ri->bits_per_channel = 8; + else if (p->depth == 16) + ri->bits_per_channel = 16; + else + return stbi__errpuc("bad bits_per_channel", "PNG not supported: unsupported color depth"); + result = p->out; + p->out = NULL; + if (req_comp && req_comp != p->s->img_out_n) { + if (ri->bits_per_channel == 8) + result = stbi__convert_format((unsigned char *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + else + result = stbi__convert_format16((stbi__uint16 *) result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + p->s->img_out_n = req_comp; + if (result == NULL) return result; + } + *x = p->s->img_x; + *y = p->s->img_y; + if (n) *n = p->s->img_n; + } + STBI_FREE(p->out); p->out = NULL; + STBI_FREE(p->expanded); p->expanded = NULL; + STBI_FREE(p->idata); p->idata = NULL; + + return result; +} + +static void *stbi__png_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi__png p; + p.s = s; + return stbi__do_png(&p, x,y,comp,req_comp, ri); +} + +static int stbi__png_test(stbi__context *s) +{ + int r; + r = stbi__check_png_header(s); + stbi__rewind(s); + return r; +} + +static int stbi__png_info_raw(stbi__png *p, int *x, int *y, int *comp) +{ + if (!stbi__parse_png_file(p, STBI__SCAN_header, 0)) { + stbi__rewind( p->s ); + return 0; + } + if (x) *x = p->s->img_x; + if (y) *y = p->s->img_y; + if (comp) *comp = p->s->img_n; + return 1; +} + +static int stbi__png_info(stbi__context *s, int *x, int *y, int *comp) +{ + stbi__png p; + p.s = s; + return stbi__png_info_raw(&p, x, y, comp); +} + +static int stbi__png_is16(stbi__context *s) +{ + stbi__png p; + p.s = s; + if (!stbi__png_info_raw(&p, NULL, NULL, NULL)) + return 0; + if (p.depth != 16) { + stbi__rewind(p.s); + return 0; + } + return 1; +} +#endif + +// Microsoft/Windows BMP image + +#ifndef STBI_NO_BMP +static int stbi__bmp_test_raw(stbi__context *s) +{ + int r; + int sz; + if (stbi__get8(s) != 'B') return 0; + if (stbi__get8(s) != 'M') return 0; + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + stbi__get32le(s); // discard data offset + sz = stbi__get32le(s); + r = (sz == 12 || sz == 40 || sz == 56 || sz == 108 || sz == 124); + return r; +} + +static int stbi__bmp_test(stbi__context *s) +{ + int r = stbi__bmp_test_raw(s); + stbi__rewind(s); + return r; +} + + +// returns 0..31 for the highest set bit +static int stbi__high_bit(unsigned int z) +{ + int n=0; + if (z == 0) return -1; + if (z >= 0x10000) { n += 16; z >>= 16; } + if (z >= 0x00100) { n += 8; z >>= 8; } + if (z >= 0x00010) { n += 4; z >>= 4; } + if (z >= 0x00004) { n += 2; z >>= 2; } + if (z >= 0x00002) { n += 1;/* >>= 1;*/ } + return n; +} + +static int stbi__bitcount(unsigned int a) +{ + a = (a & 0x55555555) + ((a >> 1) & 0x55555555); // max 2 + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // max 4 + a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits + a = (a + (a >> 8)); // max 16 per 8 bits + a = (a + (a >> 16)); // max 32 per 8 bits + return a & 0xff; +} + +// extract an arbitrarily-aligned N-bit value (N=bits) +// from v, and then make it 8-bits long and fractionally +// extend it to full full range. +static int stbi__shiftsigned(unsigned int v, int shift, int bits) +{ + static unsigned int mul_table[9] = { + 0, + 0xff/*0b11111111*/, 0x55/*0b01010101*/, 0x49/*0b01001001*/, 0x11/*0b00010001*/, + 0x21/*0b00100001*/, 0x41/*0b01000001*/, 0x81/*0b10000001*/, 0x01/*0b00000001*/, + }; + static unsigned int shift_table[9] = { + 0, 0,0,1,0,2,4,6,0, + }; + if (shift < 0) + v <<= -shift; + else + v >>= shift; + STBI_ASSERT(v < 256); + v >>= (8-bits); + STBI_ASSERT(bits >= 0 && bits <= 8); + return (int) ((unsigned) v * mul_table[bits]) >> shift_table[bits]; +} + +typedef struct +{ + int bpp, offset, hsz; + unsigned int mr,mg,mb,ma, all_a; + int extra_read; +} stbi__bmp_data; + +static int stbi__bmp_set_mask_defaults(stbi__bmp_data *info, int compress) +{ + // BI_BITFIELDS specifies masks explicitly, don't override + if (compress == 3) + return 1; + + if (compress == 0) { + if (info->bpp == 16) { + info->mr = 31u << 10; + info->mg = 31u << 5; + info->mb = 31u << 0; + } else if (info->bpp == 32) { + info->mr = 0xffu << 16; + info->mg = 0xffu << 8; + info->mb = 0xffu << 0; + info->ma = 0xffu << 24; + info->all_a = 0; // if all_a is 0 at end, then we loaded alpha channel but it was all 0 + } else { + // otherwise, use defaults, which is all-0 + info->mr = info->mg = info->mb = info->ma = 0; + } + return 1; + } + return 0; // error +} + +static void *stbi__bmp_parse_header(stbi__context *s, stbi__bmp_data *info) +{ + int hsz; + if (stbi__get8(s) != 'B' || stbi__get8(s) != 'M') return stbi__errpuc("not BMP", "Corrupt BMP"); + stbi__get32le(s); // discard filesize + stbi__get16le(s); // discard reserved + stbi__get16le(s); // discard reserved + info->offset = stbi__get32le(s); + info->hsz = hsz = stbi__get32le(s); + info->mr = info->mg = info->mb = info->ma = 0; + info->extra_read = 14; + + if (info->offset < 0) return stbi__errpuc("bad BMP", "bad BMP"); + + if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108 && hsz != 124) return stbi__errpuc("unknown BMP", "BMP type not supported: unknown"); + if (hsz == 12) { + s->img_x = stbi__get16le(s); + s->img_y = stbi__get16le(s); + } else { + s->img_x = stbi__get32le(s); + s->img_y = stbi__get32le(s); + } + if (stbi__get16le(s) != 1) return stbi__errpuc("bad BMP", "bad BMP"); + info->bpp = stbi__get16le(s); + if (hsz != 12) { + int compress = stbi__get32le(s); + if (compress == 1 || compress == 2) return stbi__errpuc("BMP RLE", "BMP type not supported: RLE"); + if (compress >= 4) return stbi__errpuc("BMP JPEG/PNG", "BMP type not supported: unsupported compression"); // this includes PNG/JPEG modes + if (compress == 3 && info->bpp != 16 && info->bpp != 32) return stbi__errpuc("bad BMP", "bad BMP"); // bitfields requires 16 or 32 bits/pixel + stbi__get32le(s); // discard sizeof + stbi__get32le(s); // discard hres + stbi__get32le(s); // discard vres + stbi__get32le(s); // discard colorsused + stbi__get32le(s); // discard max important + if (hsz == 40 || hsz == 56) { + if (hsz == 56) { + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + stbi__get32le(s); + } + if (info->bpp == 16 || info->bpp == 32) { + if (compress == 0) { + stbi__bmp_set_mask_defaults(info, compress); + } else if (compress == 3) { + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->extra_read += 12; + // not documented, but generated by photoshop and handled by mspaint + if (info->mr == info->mg && info->mg == info->mb) { + // ?!?!? + return stbi__errpuc("bad BMP", "bad BMP"); + } + } else + return stbi__errpuc("bad BMP", "bad BMP"); + } + } else { + // V4/V5 header + int i; + if (hsz != 108 && hsz != 124) + return stbi__errpuc("bad BMP", "bad BMP"); + info->mr = stbi__get32le(s); + info->mg = stbi__get32le(s); + info->mb = stbi__get32le(s); + info->ma = stbi__get32le(s); + if (compress != 3) // override mr/mg/mb unless in BI_BITFIELDS mode, as per docs + stbi__bmp_set_mask_defaults(info, compress); + stbi__get32le(s); // discard color space + for (i=0; i < 12; ++i) + stbi__get32le(s); // discard color space parameters + if (hsz == 124) { + stbi__get32le(s); // discard rendering intent + stbi__get32le(s); // discard offset of profile data + stbi__get32le(s); // discard size of profile data + stbi__get32le(s); // discard reserved + } + } + } + return (void *) 1; +} + + +static void *stbi__bmp_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *out; + unsigned int mr=0,mg=0,mb=0,ma=0, all_a; + stbi_uc pal[256][4]; + int psize=0,i,j,width; + int flip_vertically, pad, target; + stbi__bmp_data info; + STBI_NOTUSED(ri); + + info.all_a = 255; + if (stbi__bmp_parse_header(s, &info) == NULL) + return NULL; // error code already set + + flip_vertically = ((int) s->img_y) > 0; + s->img_y = abs((int) s->img_y); + + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + mr = info.mr; + mg = info.mg; + mb = info.mb; + ma = info.ma; + all_a = info.all_a; + + if (info.hsz == 12) { + if (info.bpp < 24) + psize = (info.offset - info.extra_read - 24) / 3; + } else { + if (info.bpp < 16) + psize = (info.offset - info.extra_read - info.hsz) >> 2; + } + if (psize == 0) { + // accept some number of extra bytes after the header, but if the offset points either to before + // the header ends or implies a large amount of extra data, reject the file as malformed + int bytes_read_so_far = s->callback_already_read + (int)(s->img_buffer - s->img_buffer_original); + int header_limit = 1024; // max we actually read is below 256 bytes currently. + int extra_data_limit = 256*4; // what ordinarily goes here is a palette; 256 entries*4 bytes is its max size. + if (bytes_read_so_far <= 0 || bytes_read_so_far > header_limit) { + return stbi__errpuc("bad header", "Corrupt BMP"); + } + // we established that bytes_read_so_far is positive and sensible. + // the first half of this test rejects offsets that are either too small positives, or + // negative, and guarantees that info.offset >= bytes_read_so_far > 0. this in turn + // ensures the number computed in the second half of the test can't overflow. + if (info.offset < bytes_read_so_far || info.offset - bytes_read_so_far > extra_data_limit) { + return stbi__errpuc("bad offset", "Corrupt BMP"); + } else { + stbi__skip(s, info.offset - bytes_read_so_far); + } + } + + if (info.bpp == 24 && ma == 0xff000000) + s->img_n = 3; + else + s->img_n = ma ? 4 : 3; + if (req_comp && req_comp >= 3) // we can directly decode 3 or 4 + target = req_comp; + else + target = s->img_n; // if they want monochrome, we'll post-convert + + // sanity-check size + if (!stbi__mad3sizes_valid(target, s->img_x, s->img_y, 0)) + return stbi__errpuc("too large", "Corrupt BMP"); + + out = (stbi_uc *) stbi__malloc_mad3(target, s->img_x, s->img_y, 0); + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + if (info.bpp < 16) { + int z=0; + if (psize == 0 || psize > 256) { STBI_FREE(out); return stbi__errpuc("invalid", "Corrupt BMP"); } + for (i=0; i < psize; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + if (info.hsz != 12) stbi__get8(s); + pal[i][3] = 255; + } + stbi__skip(s, info.offset - info.extra_read - info.hsz - psize * (info.hsz == 12 ? 3 : 4)); + if (info.bpp == 1) width = (s->img_x + 7) >> 3; + else if (info.bpp == 4) width = (s->img_x + 1) >> 1; + else if (info.bpp == 8) width = s->img_x; + else { STBI_FREE(out); return stbi__errpuc("bad bpp", "Corrupt BMP"); } + pad = (-width)&3; + if (info.bpp == 1) { + for (j=0; j < (int) s->img_y; ++j) { + int bit_offset = 7, v = stbi__get8(s); + for (i=0; i < (int) s->img_x; ++i) { + int color = (v>>bit_offset)&0x1; + out[z++] = pal[color][0]; + out[z++] = pal[color][1]; + out[z++] = pal[color][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + if((--bit_offset) < 0) { + bit_offset = 7; + v = stbi__get8(s); + } + } + stbi__skip(s, pad); + } + } else { + for (j=0; j < (int) s->img_y; ++j) { + for (i=0; i < (int) s->img_x; i += 2) { + int v=stbi__get8(s),v2=0; + if (info.bpp == 4) { + v2 = v & 15; + v >>= 4; + } + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + v = (info.bpp == 8) ? stbi__get8(s) : v2; + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + } + stbi__skip(s, pad); + } + } + } else { + int rshift=0,gshift=0,bshift=0,ashift=0,rcount=0,gcount=0,bcount=0,acount=0; + int z = 0; + int easy=0; + stbi__skip(s, info.offset - info.extra_read - info.hsz); + if (info.bpp == 24) width = 3 * s->img_x; + else if (info.bpp == 16) width = 2*s->img_x; + else /* bpp = 32 and pad = 0 */ width=0; + pad = (-width) & 3; + if (info.bpp == 24) { + easy = 1; + } else if (info.bpp == 32) { + if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && ma == 0xff000000) + easy = 2; + } + if (!easy) { + if (!mr || !mg || !mb) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } + // right shift amt to put high bit in position #7 + rshift = stbi__high_bit(mr)-7; rcount = stbi__bitcount(mr); + gshift = stbi__high_bit(mg)-7; gcount = stbi__bitcount(mg); + bshift = stbi__high_bit(mb)-7; bcount = stbi__bitcount(mb); + ashift = stbi__high_bit(ma)-7; acount = stbi__bitcount(ma); + if (rcount > 8 || gcount > 8 || bcount > 8 || acount > 8) { STBI_FREE(out); return stbi__errpuc("bad masks", "Corrupt BMP"); } + } + for (j=0; j < (int) s->img_y; ++j) { + if (easy) { + for (i=0; i < (int) s->img_x; ++i) { + unsigned char a; + out[z+2] = stbi__get8(s); + out[z+1] = stbi__get8(s); + out[z+0] = stbi__get8(s); + z += 3; + a = (easy == 2 ? stbi__get8(s) : 255); + all_a |= a; + if (target == 4) out[z++] = a; + } + } else { + int bpp = info.bpp; + for (i=0; i < (int) s->img_x; ++i) { + stbi__uint32 v = (bpp == 16 ? (stbi__uint32) stbi__get16le(s) : stbi__get32le(s)); + unsigned int a; + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mr, rshift, rcount)); + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mg, gshift, gcount)); + out[z++] = STBI__BYTECAST(stbi__shiftsigned(v & mb, bshift, bcount)); + a = (ma ? stbi__shiftsigned(v & ma, ashift, acount) : 255); + all_a |= a; + if (target == 4) out[z++] = STBI__BYTECAST(a); + } + } + stbi__skip(s, pad); + } + } + + // if alpha channel is all 0s, replace with all 255s + if (target == 4 && all_a == 0) + for (i=4*s->img_x*s->img_y-1; i >= 0; i -= 4) + out[i] = 255; + + if (flip_vertically) { + stbi_uc t; + for (j=0; j < (int) s->img_y>>1; ++j) { + stbi_uc *p1 = out + j *s->img_x*target; + stbi_uc *p2 = out + (s->img_y-1-j)*s->img_x*target; + for (i=0; i < (int) s->img_x*target; ++i) { + t = p1[i]; p1[i] = p2[i]; p2[i] = t; + } + } + } + + if (req_comp && req_comp != target) { + out = stbi__convert_format(out, target, req_comp, s->img_x, s->img_y); + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + return out; +} +#endif + +// Targa Truevision - TGA +// by Jonathan Dummer +#ifndef STBI_NO_TGA +// returns STBI_rgb or whatever, 0 on error +static int stbi__tga_get_comp(int bits_per_pixel, int is_grey, int* is_rgb16) +{ + // only RGB or RGBA (incl. 16bit) or grey allowed + if (is_rgb16) *is_rgb16 = 0; + switch(bits_per_pixel) { + case 8: return STBI_grey; + case 16: if(is_grey) return STBI_grey_alpha; + // fallthrough + case 15: if(is_rgb16) *is_rgb16 = 1; + return STBI_rgb; + case 24: // fallthrough + case 32: return bits_per_pixel/8; + default: return 0; + } +} + +static int stbi__tga_info(stbi__context *s, int *x, int *y, int *comp) +{ + int tga_w, tga_h, tga_comp, tga_image_type, tga_bits_per_pixel, tga_colormap_bpp; + int sz, tga_colormap_type; + stbi__get8(s); // discard Offset + tga_colormap_type = stbi__get8(s); // colormap type + if( tga_colormap_type > 1 ) { + stbi__rewind(s); + return 0; // only RGB or indexed allowed + } + tga_image_type = stbi__get8(s); // image type + if ( tga_colormap_type == 1 ) { // colormapped (paletted) image + if (tga_image_type != 1 && tga_image_type != 9) { + stbi__rewind(s); + return 0; + } + stbi__skip(s,4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) { + stbi__rewind(s); + return 0; + } + stbi__skip(s,4); // skip image x and y origin + tga_colormap_bpp = sz; + } else { // "normal" image w/o colormap - only RGB or grey allowed, +/- RLE + if ( (tga_image_type != 2) && (tga_image_type != 3) && (tga_image_type != 10) && (tga_image_type != 11) ) { + stbi__rewind(s); + return 0; // only RGB or grey allowed, +/- RLE + } + stbi__skip(s,9); // skip colormap specification and image x/y origin + tga_colormap_bpp = 0; + } + tga_w = stbi__get16le(s); + if( tga_w < 1 ) { + stbi__rewind(s); + return 0; // test width + } + tga_h = stbi__get16le(s); + if( tga_h < 1 ) { + stbi__rewind(s); + return 0; // test height + } + tga_bits_per_pixel = stbi__get8(s); // bits per pixel + stbi__get8(s); // ignore alpha bits + if (tga_colormap_bpp != 0) { + if((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16)) { + // when using a colormap, tga_bits_per_pixel is the size of the indexes + // I don't think anything but 8 or 16bit indexes makes sense + stbi__rewind(s); + return 0; + } + tga_comp = stbi__tga_get_comp(tga_colormap_bpp, 0, NULL); + } else { + tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3) || (tga_image_type == 11), NULL); + } + if(!tga_comp) { + stbi__rewind(s); + return 0; + } + if (x) *x = tga_w; + if (y) *y = tga_h; + if (comp) *comp = tga_comp; + return 1; // seems to have passed everything +} + +static int stbi__tga_test(stbi__context *s) +{ + int res = 0; + int sz, tga_color_type; + stbi__get8(s); // discard Offset + tga_color_type = stbi__get8(s); // color type + if ( tga_color_type > 1 ) goto errorEnd; // only RGB or indexed allowed + sz = stbi__get8(s); // image type + if ( tga_color_type == 1 ) { // colormapped (paletted) image + if (sz != 1 && sz != 9) goto errorEnd; // colortype 1 demands image type 1 or 9 + stbi__skip(s,4); // skip index of first colormap entry and number of entries + sz = stbi__get8(s); // check bits per palette color entry + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; + stbi__skip(s,4); // skip image x and y origin + } else { // "normal" image w/o colormap + if ( (sz != 2) && (sz != 3) && (sz != 10) && (sz != 11) ) goto errorEnd; // only RGB or grey allowed, +/- RLE + stbi__skip(s,9); // skip colormap specification and image x/y origin + } + if ( stbi__get16le(s) < 1 ) goto errorEnd; // test width + if ( stbi__get16le(s) < 1 ) goto errorEnd; // test height + sz = stbi__get8(s); // bits per pixel + if ( (tga_color_type == 1) && (sz != 8) && (sz != 16) ) goto errorEnd; // for colormapped images, bpp is size of an index + if ( (sz != 8) && (sz != 15) && (sz != 16) && (sz != 24) && (sz != 32) ) goto errorEnd; + + res = 1; // if we got this far, everything's good and we can return 1 instead of 0 + +errorEnd: + stbi__rewind(s); + return res; +} + +// read 16bit value and convert to 24bit RGB +static void stbi__tga_read_rgb16(stbi__context *s, stbi_uc* out) +{ + stbi__uint16 px = (stbi__uint16)stbi__get16le(s); + stbi__uint16 fiveBitMask = 31; + // we have 3 channels with 5bits each + int r = (px >> 10) & fiveBitMask; + int g = (px >> 5) & fiveBitMask; + int b = px & fiveBitMask; + // Note that this saves the data in RGB(A) order, so it doesn't need to be swapped later + out[0] = (stbi_uc)((r * 255)/31); + out[1] = (stbi_uc)((g * 255)/31); + out[2] = (stbi_uc)((b * 255)/31); + + // some people claim that the most significant bit might be used for alpha + // (possibly if an alpha-bit is set in the "image descriptor byte") + // but that only made 16bit test images completely translucent.. + // so let's treat all 15 and 16bit TGAs as RGB with no alpha. +} + +static void *stbi__tga_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + // read in the TGA header stuff + int tga_offset = stbi__get8(s); + int tga_indexed = stbi__get8(s); + int tga_image_type = stbi__get8(s); + int tga_is_RLE = 0; + int tga_palette_start = stbi__get16le(s); + int tga_palette_len = stbi__get16le(s); + int tga_palette_bits = stbi__get8(s); + int tga_x_origin = stbi__get16le(s); + int tga_y_origin = stbi__get16le(s); + int tga_width = stbi__get16le(s); + int tga_height = stbi__get16le(s); + int tga_bits_per_pixel = stbi__get8(s); + int tga_comp, tga_rgb16=0; + int tga_inverted = stbi__get8(s); + // int tga_alpha_bits = tga_inverted & 15; // the 4 lowest bits - unused (useless?) + // image data + unsigned char *tga_data; + unsigned char *tga_palette = NULL; + int i, j; + unsigned char raw_data[4] = {0}; + int RLE_count = 0; + int RLE_repeating = 0; + int read_next_pixel = 1; + STBI_NOTUSED(ri); + STBI_NOTUSED(tga_x_origin); // @TODO + STBI_NOTUSED(tga_y_origin); // @TODO + + if (tga_height > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (tga_width > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + // do a tiny bit of precessing + if ( tga_image_type >= 8 ) + { + tga_image_type -= 8; + tga_is_RLE = 1; + } + tga_inverted = 1 - ((tga_inverted >> 5) & 1); + + // If I'm paletted, then I'll use the number of bits from the palette + if ( tga_indexed ) tga_comp = stbi__tga_get_comp(tga_palette_bits, 0, &tga_rgb16); + else tga_comp = stbi__tga_get_comp(tga_bits_per_pixel, (tga_image_type == 3), &tga_rgb16); + + if(!tga_comp) // shouldn't really happen, stbi__tga_test() should have ensured basic consistency + return stbi__errpuc("bad format", "Can't find out TGA pixelformat"); + + // tga info + *x = tga_width; + *y = tga_height; + if (comp) *comp = tga_comp; + + if (!stbi__mad3sizes_valid(tga_width, tga_height, tga_comp, 0)) + return stbi__errpuc("too large", "Corrupt TGA"); + + tga_data = (unsigned char*)stbi__malloc_mad3(tga_width, tga_height, tga_comp, 0); + if (!tga_data) return stbi__errpuc("outofmem", "Out of memory"); + + // skip to the data's starting position (offset usually = 0) + stbi__skip(s, tga_offset ); + + if ( !tga_indexed && !tga_is_RLE && !tga_rgb16 ) { + for (i=0; i < tga_height; ++i) { + int row = tga_inverted ? tga_height -i - 1 : i; + stbi_uc *tga_row = tga_data + row*tga_width*tga_comp; + stbi__getn(s, tga_row, tga_width * tga_comp); + } + } else { + // do I need to load a palette? + if ( tga_indexed) + { + if (tga_palette_len == 0) { /* you have to have at least one entry! */ + STBI_FREE(tga_data); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + + // any data to skip? (offset usually = 0) + stbi__skip(s, tga_palette_start ); + // load the palette + tga_palette = (unsigned char*)stbi__malloc_mad2(tga_palette_len, tga_comp, 0); + if (!tga_palette) { + STBI_FREE(tga_data); + return stbi__errpuc("outofmem", "Out of memory"); + } + if (tga_rgb16) { + stbi_uc *pal_entry = tga_palette; + STBI_ASSERT(tga_comp == STBI_rgb); + for (i=0; i < tga_palette_len; ++i) { + stbi__tga_read_rgb16(s, pal_entry); + pal_entry += tga_comp; + } + } else if (!stbi__getn(s, tga_palette, tga_palette_len * tga_comp)) { + STBI_FREE(tga_data); + STBI_FREE(tga_palette); + return stbi__errpuc("bad palette", "Corrupt TGA"); + } + } + // load the data + for (i=0; i < tga_width * tga_height; ++i) + { + // if I'm in RLE mode, do I need to get a RLE stbi__pngchunk? + if ( tga_is_RLE ) + { + if ( RLE_count == 0 ) + { + // yep, get the next byte as a RLE command + int RLE_cmd = stbi__get8(s); + RLE_count = 1 + (RLE_cmd & 127); + RLE_repeating = RLE_cmd >> 7; + read_next_pixel = 1; + } else if ( !RLE_repeating ) + { + read_next_pixel = 1; + } + } else + { + read_next_pixel = 1; + } + // OK, if I need to read a pixel, do it now + if ( read_next_pixel ) + { + // load however much data we did have + if ( tga_indexed ) + { + // read in index, then perform the lookup + int pal_idx = (tga_bits_per_pixel == 8) ? stbi__get8(s) : stbi__get16le(s); + if ( pal_idx >= tga_palette_len ) { + // invalid index + pal_idx = 0; + } + pal_idx *= tga_comp; + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = tga_palette[pal_idx+j]; + } + } else if(tga_rgb16) { + STBI_ASSERT(tga_comp == STBI_rgb); + stbi__tga_read_rgb16(s, raw_data); + } else { + // read in the data raw + for (j = 0; j < tga_comp; ++j) { + raw_data[j] = stbi__get8(s); + } + } + // clear the reading flag for the next pixel + read_next_pixel = 0; + } // end of reading a pixel + + // copy data + for (j = 0; j < tga_comp; ++j) + tga_data[i*tga_comp+j] = raw_data[j]; + + // in case we're in RLE mode, keep counting down + --RLE_count; + } + // do I need to invert the image? + if ( tga_inverted ) + { + for (j = 0; j*2 < tga_height; ++j) + { + int index1 = j * tga_width * tga_comp; + int index2 = (tga_height - 1 - j) * tga_width * tga_comp; + for (i = tga_width * tga_comp; i > 0; --i) + { + unsigned char temp = tga_data[index1]; + tga_data[index1] = tga_data[index2]; + tga_data[index2] = temp; + ++index1; + ++index2; + } + } + } + // clear my palette, if I had one + if ( tga_palette != NULL ) + { + STBI_FREE( tga_palette ); + } + } + + // swap RGB - if the source data was RGB16, it already is in the right order + if (tga_comp >= 3 && !tga_rgb16) + { + unsigned char* tga_pixel = tga_data; + for (i=0; i < tga_width * tga_height; ++i) + { + unsigned char temp = tga_pixel[0]; + tga_pixel[0] = tga_pixel[2]; + tga_pixel[2] = temp; + tga_pixel += tga_comp; + } + } + + // convert to target component count + if (req_comp && req_comp != tga_comp) + tga_data = stbi__convert_format(tga_data, tga_comp, req_comp, tga_width, tga_height); + + // the things I do to get rid of an error message, and yet keep + // Microsoft's C compilers happy... [8^( + tga_palette_start = tga_palette_len = tga_palette_bits = + tga_x_origin = tga_y_origin = 0; + STBI_NOTUSED(tga_palette_start); + // OK, done + return tga_data; +} +#endif + +// ************************************************************************************************* +// Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, tweaked by STB + +#ifndef STBI_NO_PSD +static int stbi__psd_test(stbi__context *s) +{ + int r = (stbi__get32be(s) == 0x38425053); + stbi__rewind(s); + return r; +} + +static int stbi__psd_decode_rle(stbi__context *s, stbi_uc *p, int pixelCount) +{ + int count, nleft, len; + + count = 0; + while ((nleft = pixelCount - count) > 0) { + len = stbi__get8(s); + if (len == 128) { + // No-op. + } else if (len < 128) { + // Copy next len+1 bytes literally. + len++; + if (len > nleft) return 0; // corrupt data + count += len; + while (len) { + *p = stbi__get8(s); + p += 4; + len--; + } + } else if (len > 128) { + stbi_uc val; + // Next -len+1 bytes in the dest are replicated from next source byte. + // (Interpret len as a negative 8-bit int.) + len = 257 - len; + if (len > nleft) return 0; // corrupt data + val = stbi__get8(s); + count += len; + while (len) { + *p = val; + p += 4; + len--; + } + } + } + + return 1; +} + +static void *stbi__psd_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +{ + int pixelCount; + int channelCount, compression; + int channel, i; + int bitdepth; + int w,h; + stbi_uc *out; + STBI_NOTUSED(ri); + + // Check identifier + if (stbi__get32be(s) != 0x38425053) // "8BPS" + return stbi__errpuc("not PSD", "Corrupt PSD image"); + + // Check file type version. + if (stbi__get16be(s) != 1) + return stbi__errpuc("wrong version", "Unsupported version of PSD image"); + + // Skip 6 reserved bytes. + stbi__skip(s, 6 ); + + // Read the number of channels (R, G, B, A, etc). + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) + return stbi__errpuc("wrong channel count", "Unsupported number of channels in PSD image"); + + // Read the rows and columns of the image. + h = stbi__get32be(s); + w = stbi__get32be(s); + + if (h > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (w > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + // Make sure the depth is 8 bits. + bitdepth = stbi__get16be(s); + if (bitdepth != 8 && bitdepth != 16) + return stbi__errpuc("unsupported bit depth", "PSD bit depth is not 8 or 16 bit"); + + // Make sure the color mode is RGB. + // Valid options are: + // 0: Bitmap + // 1: Grayscale + // 2: Indexed color + // 3: RGB color + // 4: CMYK color + // 7: Multichannel + // 8: Duotone + // 9: Lab color + if (stbi__get16be(s) != 3) + return stbi__errpuc("wrong color format", "PSD is not in RGB color format"); + + // Skip the Mode Data. (It's the palette for indexed color; other info for other modes.) + stbi__skip(s,stbi__get32be(s) ); + + // Skip the image resources. (resolution, pen tool paths, etc) + stbi__skip(s, stbi__get32be(s) ); + + // Skip the reserved data. + stbi__skip(s, stbi__get32be(s) ); + + // Find out if the data is compressed. + // Known values: + // 0: no compression + // 1: RLE compressed + compression = stbi__get16be(s); + if (compression > 1) + return stbi__errpuc("bad compression", "PSD has an unknown compression format"); + + // Check size + if (!stbi__mad3sizes_valid(4, w, h, 0)) + return stbi__errpuc("too large", "Corrupt PSD"); + + // Create the destination image. + + if (!compression && bitdepth == 16 && bpc == 16) { + out = (stbi_uc *) stbi__malloc_mad3(8, w, h, 0); + ri->bits_per_channel = 16; + } else + out = (stbi_uc *) stbi__malloc(4 * w*h); + + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + pixelCount = w*h; + + // Initialize the data to zero. + //memset( out, 0, pixelCount * 4 ); + + // Finally, the image data. + if (compression) { + // RLE as used by .PSD and .TIFF + // Loop until you get the number of unpacked bytes you are expecting: + // Read the next source byte into n. + // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. + // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times. + // Else if n is 128, noop. + // Endloop + + // The RLE-compressed data is preceded by a 2-byte data count for each row in the data, + // which we're going to just skip. + stbi__skip(s, h * channelCount * 2 ); + + // Read the RLE data by channel. + for (channel = 0; channel < 4; channel++) { + stbi_uc *p; + + p = out+channel; + if (channel >= channelCount) { + // Fill this channel with default data. + for (i = 0; i < pixelCount; i++, p += 4) + *p = (channel == 3 ? 255 : 0); + } else { + // Read the RLE data. + if (!stbi__psd_decode_rle(s, p, pixelCount)) { + STBI_FREE(out); + return stbi__errpuc("corrupt", "bad RLE data"); + } + } + } + + } else { + // We're at the raw image data. It's each channel in order (Red, Green, Blue, Alpha, ...) + // where each channel consists of an 8-bit (or 16-bit) value for each pixel in the image. + + // Read the data by channel. + for (channel = 0; channel < 4; channel++) { + if (channel >= channelCount) { + // Fill this channel with default data. + if (bitdepth == 16 && bpc == 16) { + stbi__uint16 *q = ((stbi__uint16 *) out) + channel; + stbi__uint16 val = channel == 3 ? 65535 : 0; + for (i = 0; i < pixelCount; i++, q += 4) + *q = val; + } else { + stbi_uc *p = out+channel; + stbi_uc val = channel == 3 ? 255 : 0; + for (i = 0; i < pixelCount; i++, p += 4) + *p = val; + } + } else { + if (ri->bits_per_channel == 16) { // output bpc + stbi__uint16 *q = ((stbi__uint16 *) out) + channel; + for (i = 0; i < pixelCount; i++, q += 4) + *q = (stbi__uint16) stbi__get16be(s); + } else { + stbi_uc *p = out+channel; + if (bitdepth == 16) { // input bpc + for (i = 0; i < pixelCount; i++, p += 4) + *p = (stbi_uc) (stbi__get16be(s) >> 8); + } else { + for (i = 0; i < pixelCount; i++, p += 4) + *p = stbi__get8(s); + } + } + } + } + } + + // remove weird white matte from PSD + if (channelCount >= 4) { + if (ri->bits_per_channel == 16) { + for (i=0; i < w*h; ++i) { + stbi__uint16 *pixel = (stbi__uint16 *) out + 4*i; + if (pixel[3] != 0 && pixel[3] != 65535) { + float a = pixel[3] / 65535.0f; + float ra = 1.0f / a; + float inv_a = 65535.0f * (1 - ra); + pixel[0] = (stbi__uint16) (pixel[0]*ra + inv_a); + pixel[1] = (stbi__uint16) (pixel[1]*ra + inv_a); + pixel[2] = (stbi__uint16) (pixel[2]*ra + inv_a); + } + } + } else { + for (i=0; i < w*h; ++i) { + unsigned char *pixel = out + 4*i; + if (pixel[3] != 0 && pixel[3] != 255) { + float a = pixel[3] / 255.0f; + float ra = 1.0f / a; + float inv_a = 255.0f * (1 - ra); + pixel[0] = (unsigned char) (pixel[0]*ra + inv_a); + pixel[1] = (unsigned char) (pixel[1]*ra + inv_a); + pixel[2] = (unsigned char) (pixel[2]*ra + inv_a); + } + } + } + } + + // convert to desired output format + if (req_comp && req_comp != 4) { + if (ri->bits_per_channel == 16) + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, 4, req_comp, w, h); + else + out = stbi__convert_format(out, 4, req_comp, w, h); + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + + if (comp) *comp = 4; + *y = h; + *x = w; + + return out; +} +#endif + +// ************************************************************************************************* +// Softimage PIC loader +// by Tom Seddon +// +// See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format +// See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/ + +#ifndef STBI_NO_PIC +static int stbi__pic_is4(stbi__context *s,const char *str) +{ + int i; + for (i=0; i<4; ++i) + if (stbi__get8(s) != (stbi_uc)str[i]) + return 0; + + return 1; +} + +static int stbi__pic_test_core(stbi__context *s) +{ + int i; + + if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) + return 0; + + for(i=0;i<84;++i) + stbi__get8(s); + + if (!stbi__pic_is4(s,"PICT")) + return 0; + + return 1; +} + +typedef struct +{ + stbi_uc size,type,channel; +} stbi__pic_packet; + +static stbi_uc *stbi__readval(stbi__context *s, int channel, stbi_uc *dest) +{ + int mask=0x80, i; + + for (i=0; i<4; ++i, mask>>=1) { + if (channel & mask) { + if (stbi__at_eof(s)) return stbi__errpuc("bad file","PIC file too short"); + dest[i]=stbi__get8(s); + } + } + + return dest; +} + +static void stbi__copyval(int channel,stbi_uc *dest,const stbi_uc *src) +{ + int mask=0x80,i; + + for (i=0;i<4; ++i, mask>>=1) + if (channel&mask) + dest[i]=src[i]; +} + +static stbi_uc *stbi__pic_load_core(stbi__context *s,int width,int height,int *comp, stbi_uc *result) +{ + int act_comp=0,num_packets=0,y,chained; + stbi__pic_packet packets[10]; + + // this will (should...) cater for even some bizarre stuff like having data + // for the same channel in multiple packets. + do { + stbi__pic_packet *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return stbi__errpuc("bad format","too many packets"); + + packet = &packets[num_packets++]; + + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + + act_comp |= packet->channel; + + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (reading packets)"); + if (packet->size != 8) return stbi__errpuc("bad format","packet isn't 8bpp"); + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel? + + for(y=0; ytype) { + default: + return stbi__errpuc("bad format","packet has bad compression type"); + + case 0: {//uncompressed + int x; + + for(x=0;xchannel,dest)) + return 0; + break; + } + + case 1://Pure RLE + { + int left=width, i; + + while (left>0) { + stbi_uc count,value[4]; + + count=stbi__get8(s); + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pure read count)"); + + if (count > left) + count = (stbi_uc) left; + + if (!stbi__readval(s,packet->channel,value)) return 0; + + for(i=0; ichannel,dest,value); + left -= count; + } + } + break; + + case 2: {//Mixed RLE + int left=width; + while (left>0) { + int count = stbi__get8(s), i; + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (mixed read count)"); + + if (count >= 128) { // Repeated + stbi_uc value[4]; + + if (count==128) + count = stbi__get16be(s); + else + count -= 127; + if (count > left) + return stbi__errpuc("bad file","scanline overrun"); + + if (!stbi__readval(s,packet->channel,value)) + return 0; + + for(i=0;ichannel,dest,value); + } else { // Raw + ++count; + if (count>left) return stbi__errpuc("bad file","scanline overrun"); + + for(i=0;ichannel,dest)) + return 0; + } + left-=count; + } + break; + } + } + } + } + + return result; +} + +static void *stbi__pic_load(stbi__context *s,int *px,int *py,int *comp,int req_comp, stbi__result_info *ri) +{ + stbi_uc *result; + int i, x,y, internal_comp; + STBI_NOTUSED(ri); + + if (!comp) comp = &internal_comp; + + for (i=0; i<92; ++i) + stbi__get8(s); + + x = stbi__get16be(s); + y = stbi__get16be(s); + + if (y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + if (stbi__at_eof(s)) return stbi__errpuc("bad file","file too short (pic header)"); + if (!stbi__mad3sizes_valid(x, y, 4, 0)) return stbi__errpuc("too large", "PIC image too large to decode"); + + stbi__get32be(s); //skip `ratio' + stbi__get16be(s); //skip `fields' + stbi__get16be(s); //skip `pad' + + // intermediate buffer is RGBA + result = (stbi_uc *) stbi__malloc_mad3(x, y, 4, 0); + if (!result) return stbi__errpuc("outofmem", "Out of memory"); + memset(result, 0xff, x*y*4); + + if (!stbi__pic_load_core(s,x,y,comp, result)) { + STBI_FREE(result); + result=0; + } + *px = x; + *py = y; + if (req_comp == 0) req_comp = *comp; + result=stbi__convert_format(result,4,req_comp,x,y); + + return result; +} + +static int stbi__pic_test(stbi__context *s) +{ + int r = stbi__pic_test_core(s); + stbi__rewind(s); + return r; +} +#endif + +// ************************************************************************************************* +// GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb + +#ifndef STBI_NO_GIF +typedef struct +{ + stbi__int16 prefix; + stbi_uc first; + stbi_uc suffix; +} stbi__gif_lzw; + +typedef struct +{ + int w,h; + stbi_uc *out; // output buffer (always 4 components) + stbi_uc *background; // The current "background" as far as a gif is concerned + stbi_uc *history; + int flags, bgindex, ratio, transparent, eflags; + stbi_uc pal[256][4]; + stbi_uc lpal[256][4]; + stbi__gif_lzw codes[8192]; + stbi_uc *color_table; + int parse, step; + int lflags; + int start_x, start_y; + int max_x, max_y; + int cur_x, cur_y; + int line_size; + int delay; +} stbi__gif; + +static int stbi__gif_test_raw(stbi__context *s) +{ + int sz; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') return 0; + sz = stbi__get8(s); + if (sz != '9' && sz != '7') return 0; + if (stbi__get8(s) != 'a') return 0; + return 1; +} + +static int stbi__gif_test(stbi__context *s) +{ + int r = stbi__gif_test_raw(s); + stbi__rewind(s); + return r; +} + +static void stbi__gif_parse_colortable(stbi__context *s, stbi_uc pal[256][4], int num_entries, int transp) +{ + int i; + for (i=0; i < num_entries; ++i) { + pal[i][2] = stbi__get8(s); + pal[i][1] = stbi__get8(s); + pal[i][0] = stbi__get8(s); + pal[i][3] = transp == i ? 0 : 255; + } +} + +static int stbi__gif_header(stbi__context *s, stbi__gif *g, int *comp, int is_info) +{ + stbi_uc version; + if (stbi__get8(s) != 'G' || stbi__get8(s) != 'I' || stbi__get8(s) != 'F' || stbi__get8(s) != '8') + return stbi__err("not GIF", "Corrupt GIF"); + + version = stbi__get8(s); + if (version != '7' && version != '9') return stbi__err("not GIF", "Corrupt GIF"); + if (stbi__get8(s) != 'a') return stbi__err("not GIF", "Corrupt GIF"); + + stbi__g_failure_reason = ""; + g->w = stbi__get16le(s); + g->h = stbi__get16le(s); + g->flags = stbi__get8(s); + g->bgindex = stbi__get8(s); + g->ratio = stbi__get8(s); + g->transparent = -1; + + if (g->w > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + if (g->h > STBI_MAX_DIMENSIONS) return stbi__err("too large","Very large image (corrupt?)"); + + if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments + + if (is_info) return 1; + + if (g->flags & 0x80) + stbi__gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1); + + return 1; +} + +static int stbi__gif_info_raw(stbi__context *s, int *x, int *y, int *comp) +{ + stbi__gif* g = (stbi__gif*) stbi__malloc(sizeof(stbi__gif)); + if (!g) return stbi__err("outofmem", "Out of memory"); + if (!stbi__gif_header(s, g, comp, 1)) { + STBI_FREE(g); + stbi__rewind( s ); + return 0; + } + if (x) *x = g->w; + if (y) *y = g->h; + STBI_FREE(g); + return 1; +} + +static void stbi__out_gif_code(stbi__gif *g, stbi__uint16 code) +{ + stbi_uc *p, *c; + int idx; + + // recurse to decode the prefixes, since the linked-list is backwards, + // and working backwards through an interleaved image would be nasty + if (g->codes[code].prefix >= 0) + stbi__out_gif_code(g, g->codes[code].prefix); + + if (g->cur_y >= g->max_y) return; + + idx = g->cur_x + g->cur_y; + p = &g->out[idx]; + g->history[idx / 4] = 1; + + c = &g->color_table[g->codes[code].suffix * 4]; + if (c[3] > 128) { // don't render transparent pixels; + p[0] = c[2]; + p[1] = c[1]; + p[2] = c[0]; + p[3] = c[3]; + } + g->cur_x += 4; + + if (g->cur_x >= g->max_x) { + g->cur_x = g->start_x; + g->cur_y += g->step; + + while (g->cur_y >= g->max_y && g->parse > 0) { + g->step = (1 << g->parse) * g->line_size; + g->cur_y = g->start_y + (g->step >> 1); + --g->parse; + } + } +} + +static stbi_uc *stbi__process_gif_raster(stbi__context *s, stbi__gif *g) +{ + stbi_uc lzw_cs; + stbi__int32 len, init_code; + stbi__uint32 first; + stbi__int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; + stbi__gif_lzw *p; + + lzw_cs = stbi__get8(s); + if (lzw_cs > 12) return NULL; + clear = 1 << lzw_cs; + first = 1; + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + bits = 0; + valid_bits = 0; + for (init_code = 0; init_code < clear; init_code++) { + g->codes[init_code].prefix = -1; + g->codes[init_code].first = (stbi_uc) init_code; + g->codes[init_code].suffix = (stbi_uc) init_code; + } + + // support no starting clear code + avail = clear+2; + oldcode = -1; + + len = 0; + for(;;) { + if (valid_bits < codesize) { + if (len == 0) { + len = stbi__get8(s); // start new block + if (len == 0) + return g->out; + } + --len; + bits |= (stbi__int32) stbi__get8(s) << valid_bits; + valid_bits += 8; + } else { + stbi__int32 code = bits & codemask; + bits >>= codesize; + valid_bits -= codesize; + // @OPTIMIZE: is there some way we can accelerate the non-clear path? + if (code == clear) { // clear code + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + avail = clear + 2; + oldcode = -1; + first = 0; + } else if (code == clear + 1) { // end of stream code + stbi__skip(s, len); + while ((len = stbi__get8(s)) > 0) + stbi__skip(s,len); + return g->out; + } else if (code <= avail) { + if (first) { + return stbi__errpuc("no clear code", "Corrupt GIF"); + } + + if (oldcode >= 0) { + p = &g->codes[avail++]; + if (avail > 8192) { + return stbi__errpuc("too many codes", "Corrupt GIF"); + } + + p->prefix = (stbi__int16) oldcode; + p->first = g->codes[oldcode].first; + p->suffix = (code == avail) ? p->first : g->codes[code].first; + } else if (code == avail) + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + + stbi__out_gif_code(g, (stbi__uint16) code); + + if ((avail & codemask) == 0 && avail <= 0x0FFF) { + codesize++; + codemask = (1 << codesize) - 1; + } + + oldcode = code; + } else { + return stbi__errpuc("illegal code in raster", "Corrupt GIF"); + } + } + } +} + +// this function is designed to support animated gifs, although stb_image doesn't support it +// two back is the image from two frames ago, used for a very specific disposal format +static stbi_uc *stbi__gif_load_next(stbi__context *s, stbi__gif *g, int *comp, int req_comp, stbi_uc *two_back) +{ + int dispose; + int first_frame; + int pi; + int pcount; + STBI_NOTUSED(req_comp); + + // on first frame, any non-written pixels get the background colour (non-transparent) + first_frame = 0; + if (g->out == 0) { + if (!stbi__gif_header(s, g, comp,0)) return 0; // stbi__g_failure_reason set by stbi__gif_header + if (!stbi__mad3sizes_valid(4, g->w, g->h, 0)) + return stbi__errpuc("too large", "GIF image is too large"); + pcount = g->w * g->h; + g->out = (stbi_uc *) stbi__malloc(4 * pcount); + g->background = (stbi_uc *) stbi__malloc(4 * pcount); + g->history = (stbi_uc *) stbi__malloc(pcount); + if (!g->out || !g->background || !g->history) + return stbi__errpuc("outofmem", "Out of memory"); + + // image is treated as "transparent" at the start - ie, nothing overwrites the current background; + // background colour is only used for pixels that are not rendered first frame, after that "background" + // color refers to the color that was there the previous frame. + memset(g->out, 0x00, 4 * pcount); + memset(g->background, 0x00, 4 * pcount); // state of the background (starts transparent) + memset(g->history, 0x00, pcount); // pixels that were affected previous frame + first_frame = 1; + } else { + // second frame - how do we dispose of the previous one? + dispose = (g->eflags & 0x1C) >> 2; + pcount = g->w * g->h; + + if ((dispose == 3) && (two_back == 0)) { + dispose = 2; // if I don't have an image to revert back to, default to the old background + } + + if (dispose == 3) { // use previous graphic + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy( &g->out[pi * 4], &two_back[pi * 4], 4 ); + } + } + } else if (dispose == 2) { + // restore what was changed last frame to background before that frame; + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi]) { + memcpy( &g->out[pi * 4], &g->background[pi * 4], 4 ); + } + } + } else { + // This is a non-disposal case eithe way, so just + // leave the pixels as is, and they will become the new background + // 1: do not dispose + // 0: not specified. + } + + // background is what out is after the undoing of the previou frame; + memcpy( g->background, g->out, 4 * g->w * g->h ); + } + + // clear my history; + memset( g->history, 0x00, g->w * g->h ); // pixels that were affected previous frame + + for (;;) { + int tag = stbi__get8(s); + switch (tag) { + case 0x2C: /* Image Descriptor */ + { + stbi__int32 x, y, w, h; + stbi_uc *o; + + x = stbi__get16le(s); + y = stbi__get16le(s); + w = stbi__get16le(s); + h = stbi__get16le(s); + if (((x + w) > (g->w)) || ((y + h) > (g->h))) + return stbi__errpuc("bad Image Descriptor", "Corrupt GIF"); + + g->line_size = g->w * 4; + g->start_x = x * 4; + g->start_y = y * g->line_size; + g->max_x = g->start_x + w * 4; + g->max_y = g->start_y + h * g->line_size; + g->cur_x = g->start_x; + g->cur_y = g->start_y; + + // if the width of the specified rectangle is 0, that means + // we may not see *any* pixels or the image is malformed; + // to make sure this is caught, move the current y down to + // max_y (which is what out_gif_code checks). + if (w == 0) + g->cur_y = g->max_y; + + g->lflags = stbi__get8(s); + + if (g->lflags & 0x40) { + g->step = 8 * g->line_size; // first interlaced spacing + g->parse = 3; + } else { + g->step = g->line_size; + g->parse = 0; + } + + if (g->lflags & 0x80) { + stbi__gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1); + g->color_table = (stbi_uc *) g->lpal; + } else if (g->flags & 0x80) { + g->color_table = (stbi_uc *) g->pal; + } else + return stbi__errpuc("missing color table", "Corrupt GIF"); + + o = stbi__process_gif_raster(s, g); + if (!o) return NULL; + + // if this was the first frame, + pcount = g->w * g->h; + if (first_frame && (g->bgindex > 0)) { + // if first frame, any pixel not drawn to gets the background color + for (pi = 0; pi < pcount; ++pi) { + if (g->history[pi] == 0) { + g->pal[g->bgindex][3] = 255; // just in case it was made transparent, undo that; It will be reset next frame if need be; + memcpy( &g->out[pi * 4], &g->pal[g->bgindex], 4 ); + } + } + } + + return o; + } + + case 0x21: // Comment Extension. + { + int len; + int ext = stbi__get8(s); + if (ext == 0xF9) { // Graphic Control Extension. + len = stbi__get8(s); + if (len == 4) { + g->eflags = stbi__get8(s); + g->delay = 10 * stbi__get16le(s); // delay - 1/100th of a second, saving as 1/1000ths. + + // unset old transparent + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 255; + } + if (g->eflags & 0x01) { + g->transparent = stbi__get8(s); + if (g->transparent >= 0) { + g->pal[g->transparent][3] = 0; + } + } else { + // don't need transparent + stbi__skip(s, 1); + g->transparent = -1; + } + } else { + stbi__skip(s, len); + break; + } + } + while ((len = stbi__get8(s)) != 0) { + stbi__skip(s, len); + } + break; + } + + case 0x3B: // gif stream termination code + return (stbi_uc *) s; // using '1' causes warning on some compilers + + default: + return stbi__errpuc("unknown code", "Corrupt GIF"); + } + } +} + +static void *stbi__load_gif_main_outofmem(stbi__gif *g, stbi_uc *out, int **delays) +{ + STBI_FREE(g->out); + STBI_FREE(g->history); + STBI_FREE(g->background); + + if (out) STBI_FREE(out); + if (delays && *delays) STBI_FREE(*delays); + return stbi__errpuc("outofmem", "Out of memory"); +} + +static void *stbi__load_gif_main(stbi__context *s, int **delays, int *x, int *y, int *z, int *comp, int req_comp) +{ + if (stbi__gif_test(s)) { + int layers = 0; + stbi_uc *u = 0; + stbi_uc *out = 0; + stbi_uc *two_back = 0; + stbi__gif g; + int stride; + int out_size = 0; + int delays_size = 0; + + STBI_NOTUSED(out_size); + STBI_NOTUSED(delays_size); + + memset(&g, 0, sizeof(g)); + if (delays) { + *delays = 0; + } + + do { + u = stbi__gif_load_next(s, &g, comp, req_comp, two_back); + if (u == (stbi_uc *) s) u = 0; // end of animated gif marker + + if (u) { + *x = g.w; + *y = g.h; + ++layers; + stride = g.w * g.h * 4; + + if (out) { + void *tmp = (stbi_uc*) STBI_REALLOC_SIZED( out, out_size, layers * stride ); + if (!tmp) + return stbi__load_gif_main_outofmem(&g, out, delays); + else { + out = (stbi_uc*) tmp; + out_size = layers * stride; + } + + if (delays) { + int *new_delays = (int*) STBI_REALLOC_SIZED( *delays, delays_size, sizeof(int) * layers ); + if (!new_delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + *delays = new_delays; + delays_size = layers * sizeof(int); + } + } else { + out = (stbi_uc*)stbi__malloc( layers * stride ); + if (!out) + return stbi__load_gif_main_outofmem(&g, out, delays); + out_size = layers * stride; + if (delays) { + *delays = (int*) stbi__malloc( layers * sizeof(int) ); + if (!*delays) + return stbi__load_gif_main_outofmem(&g, out, delays); + delays_size = layers * sizeof(int); + } + } + memcpy( out + ((layers - 1) * stride), u, stride ); + if (layers >= 2) { + two_back = out - 2 * stride; + } + + if (delays) { + (*delays)[layers - 1U] = g.delay; + } + } + } while (u != 0); + + // free temp buffer; + STBI_FREE(g.out); + STBI_FREE(g.history); + STBI_FREE(g.background); + + // do the final conversion after loading everything; + if (req_comp && req_comp != 4) + out = stbi__convert_format(out, 4, req_comp, layers * g.w, g.h); + + *z = layers; + return out; + } else { + return stbi__errpuc("not GIF", "Image was not as a gif type."); + } +} + +static void *stbi__gif_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *u = 0; + stbi__gif g; + memset(&g, 0, sizeof(g)); + STBI_NOTUSED(ri); + + u = stbi__gif_load_next(s, &g, comp, req_comp, 0); + if (u == (stbi_uc *) s) u = 0; // end of animated gif marker + if (u) { + *x = g.w; + *y = g.h; + + // moved conversion to after successful load so that the same + // can be done for multiple frames. + if (req_comp && req_comp != 4) + u = stbi__convert_format(u, 4, req_comp, g.w, g.h); + } else if (g.out) { + // if there was an error and we allocated an image buffer, free it! + STBI_FREE(g.out); + } + + // free buffers needed for multiple frame loading; + STBI_FREE(g.history); + STBI_FREE(g.background); + + return u; +} + +static int stbi__gif_info(stbi__context *s, int *x, int *y, int *comp) +{ + return stbi__gif_info_raw(s,x,y,comp); +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR loader +// originally by Nicolas Schulz +#ifndef STBI_NO_HDR +static int stbi__hdr_test_core(stbi__context *s, const char *signature) +{ + int i; + for (i=0; signature[i]; ++i) + if (stbi__get8(s) != signature[i]) + return 0; + stbi__rewind(s); + return 1; +} + +static int stbi__hdr_test(stbi__context* s) +{ + int r = stbi__hdr_test_core(s, "#?RADIANCE\n"); + stbi__rewind(s); + if(!r) { + r = stbi__hdr_test_core(s, "#?RGBE\n"); + stbi__rewind(s); + } + return r; +} + +#define STBI__HDR_BUFLEN 1024 +static char *stbi__hdr_gettoken(stbi__context *z, char *buffer) +{ + int len=0; + char c = '\0'; + + c = (char) stbi__get8(z); + + while (!stbi__at_eof(z) && c != '\n') { + buffer[len++] = c; + if (len == STBI__HDR_BUFLEN-1) { + // flush to end of line + while (!stbi__at_eof(z) && stbi__get8(z) != '\n') + ; + break; + } + c = (char) stbi__get8(z); + } + + buffer[len] = 0; + return buffer; +} + +static void stbi__hdr_convert(float *output, stbi_uc *input, int req_comp) +{ + if ( input[3] != 0 ) { + float f1; + // Exponent + f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8)); + if (req_comp <= 2) + output[0] = (input[0] + input[1] + input[2]) * f1 / 3; + else { + output[0] = input[0] * f1; + output[1] = input[1] * f1; + output[2] = input[2] * f1; + } + if (req_comp == 2) output[1] = 1; + if (req_comp == 4) output[3] = 1; + } else { + switch (req_comp) { + case 4: output[3] = 1; /* fallthrough */ + case 3: output[0] = output[1] = output[2] = 0; + break; + case 2: output[1] = 1; /* fallthrough */ + case 1: output[0] = 0; + break; + } + } +} + +static float *stbi__hdr_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + char buffer[STBI__HDR_BUFLEN]; + char *token; + int valid = 0; + int width, height; + stbi_uc *scanline; + float *hdr_data; + int len; + unsigned char count, value; + int i, j, k, c1,c2, z; + const char *headerToken; + STBI_NOTUSED(ri); + + // Check identifier + headerToken = stbi__hdr_gettoken(s,buffer); + if (strcmp(headerToken, "#?RADIANCE") != 0 && strcmp(headerToken, "#?RGBE") != 0) + return stbi__errpf("not HDR", "Corrupt HDR image"); + + // Parse header + for(;;) { + token = stbi__hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) return stbi__errpf("unsupported format", "Unsupported HDR format"); + + // Parse width and height + // can't use sscanf() if we're not using stdio! + token = stbi__hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + height = (int) strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) return stbi__errpf("unsupported data layout", "Unsupported HDR format"); + token += 3; + width = (int) strtol(token, NULL, 10); + + if (height > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); + if (width > STBI_MAX_DIMENSIONS) return stbi__errpf("too large","Very large image (corrupt?)"); + + *x = width; + *y = height; + + if (comp) *comp = 3; + if (req_comp == 0) req_comp = 3; + + if (!stbi__mad4sizes_valid(width, height, req_comp, sizeof(float), 0)) + return stbi__errpf("too large", "HDR image is too large"); + + // Read data + hdr_data = (float *) stbi__malloc_mad4(width, height, req_comp, sizeof(float), 0); + if (!hdr_data) + return stbi__errpf("outofmem", "Out of memory"); + + // Load image data + // image data is stored as some number of sca + if ( width < 8 || width >= 32768) { + // Read flat data + for (j=0; j < height; ++j) { + for (i=0; i < width; ++i) { + stbi_uc rgbe[4]; + main_decode_loop: + stbi__getn(s, rgbe, 4); + stbi__hdr_convert(hdr_data + j * width * req_comp + i * req_comp, rgbe, req_comp); + } + } + } else { + // Read RLE-encoded data + scanline = NULL; + + for (j = 0; j < height; ++j) { + c1 = stbi__get8(s); + c2 = stbi__get8(s); + len = stbi__get8(s); + if (c1 != 2 || c2 != 2 || (len & 0x80)) { + // not run-length encoded, so we have to actually use THIS data as a decoded + // pixel (note this can't be a valid pixel--one of RGB must be >= 128) + stbi_uc rgbe[4]; + rgbe[0] = (stbi_uc) c1; + rgbe[1] = (stbi_uc) c2; + rgbe[2] = (stbi_uc) len; + rgbe[3] = (stbi_uc) stbi__get8(s); + stbi__hdr_convert(hdr_data, rgbe, req_comp); + i = 1; + j = 0; + STBI_FREE(scanline); + goto main_decode_loop; // yes, this makes no sense + } + len <<= 8; + len |= stbi__get8(s); + if (len != width) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("invalid decoded scanline length", "corrupt HDR"); } + if (scanline == NULL) { + scanline = (stbi_uc *) stbi__malloc_mad2(width, 4, 0); + if (!scanline) { + STBI_FREE(hdr_data); + return stbi__errpf("outofmem", "Out of memory"); + } + } + + for (k = 0; k < 4; ++k) { + int nleft; + i = 0; + while ((nleft = width - i) > 0) { + count = stbi__get8(s); + if (count > 128) { + // Run + value = stbi__get8(s); + count -= 128; + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = value; + } else { + // Dump + if ((count == 0) || (count > nleft)) { STBI_FREE(hdr_data); STBI_FREE(scanline); return stbi__errpf("corrupt", "bad RLE data in HDR"); } + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = stbi__get8(s); + } + } + } + for (i=0; i < width; ++i) + stbi__hdr_convert(hdr_data+(j*width + i)*req_comp, scanline + i*4, req_comp); + } + if (scanline) + STBI_FREE(scanline); + } + + return hdr_data; +} + +static int stbi__hdr_info(stbi__context *s, int *x, int *y, int *comp) +{ + char buffer[STBI__HDR_BUFLEN]; + char *token; + int valid = 0; + int dummy; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + if (stbi__hdr_test(s) == 0) { + stbi__rewind( s ); + return 0; + } + + for(;;) { + token = stbi__hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) { + stbi__rewind( s ); + return 0; + } + token = stbi__hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) { + stbi__rewind( s ); + return 0; + } + token += 3; + *y = (int) strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) { + stbi__rewind( s ); + return 0; + } + token += 3; + *x = (int) strtol(token, NULL, 10); + *comp = 3; + return 1; +} +#endif // STBI_NO_HDR + +#ifndef STBI_NO_BMP +static int stbi__bmp_info(stbi__context *s, int *x, int *y, int *comp) +{ + void *p; + stbi__bmp_data info; + + info.all_a = 255; + p = stbi__bmp_parse_header(s, &info); + if (p == NULL) { + stbi__rewind( s ); + return 0; + } + if (x) *x = s->img_x; + if (y) *y = s->img_y; + if (comp) { + if (info.bpp == 24 && info.ma == 0xff000000) + *comp = 3; + else + *comp = info.ma ? 4 : 3; + } + return 1; +} +#endif + +#ifndef STBI_NO_PSD +static int stbi__psd_info(stbi__context *s, int *x, int *y, int *comp) +{ + int channelCount, dummy, depth; + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind( s ); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind( s ); + return 0; + } + *y = stbi__get32be(s); + *x = stbi__get32be(s); + depth = stbi__get16be(s); + if (depth != 8 && depth != 16) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 3) { + stbi__rewind( s ); + return 0; + } + *comp = 4; + return 1; +} + +static int stbi__psd_is16(stbi__context *s) +{ + int channelCount, depth; + if (stbi__get32be(s) != 0x38425053) { + stbi__rewind( s ); + return 0; + } + if (stbi__get16be(s) != 1) { + stbi__rewind( s ); + return 0; + } + stbi__skip(s, 6); + channelCount = stbi__get16be(s); + if (channelCount < 0 || channelCount > 16) { + stbi__rewind( s ); + return 0; + } + STBI_NOTUSED(stbi__get32be(s)); + STBI_NOTUSED(stbi__get32be(s)); + depth = stbi__get16be(s); + if (depth != 16) { + stbi__rewind( s ); + return 0; + } + return 1; +} +#endif + +#ifndef STBI_NO_PIC +static int stbi__pic_info(stbi__context *s, int *x, int *y, int *comp) +{ + int act_comp=0,num_packets=0,chained,dummy; + stbi__pic_packet packets[10]; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + if (!stbi__pic_is4(s,"\x53\x80\xF6\x34")) { + stbi__rewind(s); + return 0; + } + + stbi__skip(s, 88); + + *x = stbi__get16be(s); + *y = stbi__get16be(s); + if (stbi__at_eof(s)) { + stbi__rewind( s); + return 0; + } + if ( (*x) != 0 && (1 << 28) / (*x) < (*y)) { + stbi__rewind( s ); + return 0; + } + + stbi__skip(s, 8); + + do { + stbi__pic_packet *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return 0; + + packet = &packets[num_packets++]; + chained = stbi__get8(s); + packet->size = stbi__get8(s); + packet->type = stbi__get8(s); + packet->channel = stbi__get8(s); + act_comp |= packet->channel; + + if (stbi__at_eof(s)) { + stbi__rewind( s ); + return 0; + } + if (packet->size != 8) { + stbi__rewind( s ); + return 0; + } + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); + + return 1; +} +#endif + +// ************************************************************************************************* +// Portable Gray Map and Portable Pixel Map loader +// by Ken Miller +// +// PGM: http://netpbm.sourceforge.net/doc/pgm.html +// PPM: http://netpbm.sourceforge.net/doc/ppm.html +// +// Known limitations: +// Does not support comments in the header section +// Does not support ASCII image data (formats P2 and P3) + +#ifndef STBI_NO_PNM + +static int stbi__pnm_test(stbi__context *s) +{ + char p, t; + p = (char) stbi__get8(s); + t = (char) stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind( s ); + return 0; + } + return 1; +} + +static void *stbi__pnm_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +{ + stbi_uc *out; + STBI_NOTUSED(ri); + + ri->bits_per_channel = stbi__pnm_info(s, (int *)&s->img_x, (int *)&s->img_y, (int *)&s->img_n); + if (ri->bits_per_channel == 0) + return 0; + + if (s->img_y > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + if (s->img_x > STBI_MAX_DIMENSIONS) return stbi__errpuc("too large","Very large image (corrupt?)"); + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + + if (!stbi__mad4sizes_valid(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0)) + return stbi__errpuc("too large", "PNM too large"); + + out = (stbi_uc *) stbi__malloc_mad4(s->img_n, s->img_x, s->img_y, ri->bits_per_channel / 8, 0); + if (!out) return stbi__errpuc("outofmem", "Out of memory"); + if (!stbi__getn(s, out, s->img_n * s->img_x * s->img_y * (ri->bits_per_channel / 8))) { + STBI_FREE(out); + return stbi__errpuc("bad PNM", "PNM file truncated"); + } + + if (req_comp && req_comp != s->img_n) { + if (ri->bits_per_channel == 16) { + out = (stbi_uc *) stbi__convert_format16((stbi__uint16 *) out, s->img_n, req_comp, s->img_x, s->img_y); + } else { + out = stbi__convert_format(out, s->img_n, req_comp, s->img_x, s->img_y); + } + if (out == NULL) return out; // stbi__convert_format frees input on failure + } + return out; +} + +static int stbi__pnm_isspace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +static void stbi__pnm_skip_whitespace(stbi__context *s, char *c) +{ + for (;;) { + while (!stbi__at_eof(s) && stbi__pnm_isspace(*c)) + *c = (char) stbi__get8(s); + + if (stbi__at_eof(s) || *c != '#') + break; + + while (!stbi__at_eof(s) && *c != '\n' && *c != '\r' ) + *c = (char) stbi__get8(s); + } +} + +static int stbi__pnm_isdigit(char c) +{ + return c >= '0' && c <= '9'; +} + +static int stbi__pnm_getinteger(stbi__context *s, char *c) +{ + int value = 0; + + while (!stbi__at_eof(s) && stbi__pnm_isdigit(*c)) { + value = value*10 + (*c - '0'); + *c = (char) stbi__get8(s); + if((value > 214748364) || (value == 214748364 && *c > '7')) + return stbi__err("integer parse overflow", "Parsing an integer in the PPM header overflowed a 32-bit int"); + } + + return value; +} + +static int stbi__pnm_info(stbi__context *s, int *x, int *y, int *comp) +{ + int maxv, dummy; + char c, p, t; + + if (!x) x = &dummy; + if (!y) y = &dummy; + if (!comp) comp = &dummy; + + stbi__rewind(s); + + // Get identifier + p = (char) stbi__get8(s); + t = (char) stbi__get8(s); + if (p != 'P' || (t != '5' && t != '6')) { + stbi__rewind(s); + return 0; + } + + *comp = (t == '6') ? 3 : 1; // '5' is 1-component .pgm; '6' is 3-component .ppm + + c = (char) stbi__get8(s); + stbi__pnm_skip_whitespace(s, &c); + + *x = stbi__pnm_getinteger(s, &c); // read width + if(*x == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); + stbi__pnm_skip_whitespace(s, &c); + + *y = stbi__pnm_getinteger(s, &c); // read height + if (*y == 0) + return stbi__err("invalid width", "PPM image header had zero or overflowing width"); + stbi__pnm_skip_whitespace(s, &c); + + maxv = stbi__pnm_getinteger(s, &c); // read max value + if (maxv > 65535) + return stbi__err("max value > 65535", "PPM image supports only 8-bit and 16-bit images"); + else if (maxv > 255) + return 16; + else + return 8; +} + +static int stbi__pnm_is16(stbi__context *s) +{ + if (stbi__pnm_info(s, NULL, NULL, NULL) == 16) + return 1; + return 0; +} +#endif + +static int stbi__info_main(stbi__context *s, int *x, int *y, int *comp) +{ + #ifndef STBI_NO_JPEG + if (stbi__jpeg_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PNG + if (stbi__png_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_GIF + if (stbi__gif_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_BMP + if (stbi__bmp_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PSD + if (stbi__psd_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PIC + if (stbi__pic_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_PNM + if (stbi__pnm_info(s, x, y, comp)) return 1; + #endif + + #ifndef STBI_NO_HDR + if (stbi__hdr_info(s, x, y, comp)) return 1; + #endif + + // test tga last because it's a crappy test! + #ifndef STBI_NO_TGA + if (stbi__tga_info(s, x, y, comp)) + return 1; + #endif + return stbi__err("unknown image type", "Image not of any known type, or corrupt"); +} + +static int stbi__is_16_main(stbi__context *s) +{ + #ifndef STBI_NO_PNG + if (stbi__png_is16(s)) return 1; + #endif + + #ifndef STBI_NO_PSD + if (stbi__psd_is16(s)) return 1; + #endif + + #ifndef STBI_NO_PNM + if (stbi__pnm_is16(s)) return 1; + #endif + return 0; +} + +#ifndef STBI_NO_STDIO +STBIDEF int stbi_info(char const *filename, int *x, int *y, int *comp) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result; + if (!f) return stbi__err("can't fopen", "Unable to open file"); + result = stbi_info_from_file(f, x, y, comp); + fclose(f); + return result; +} + +STBIDEF int stbi_info_from_file(FILE *f, int *x, int *y, int *comp) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__info_main(&s,x,y,comp); + fseek(f,pos,SEEK_SET); + return r; +} + +STBIDEF int stbi_is_16_bit(char const *filename) +{ + FILE *f = stbi__fopen(filename, "rb"); + int result; + if (!f) return stbi__err("can't fopen", "Unable to open file"); + result = stbi_is_16_bit_from_file(f); + fclose(f); + return result; +} + +STBIDEF int stbi_is_16_bit_from_file(FILE *f) +{ + int r; + stbi__context s; + long pos = ftell(f); + stbi__start_file(&s, f); + r = stbi__is_16_main(&s); + fseek(f,pos,SEEK_SET); + return r; +} +#endif // !STBI_NO_STDIO + +STBIDEF int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__info_main(&s,x,y,comp); +} + +STBIDEF int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi__info_main(&s,x,y,comp); +} + +STBIDEF int stbi_is_16_bit_from_memory(stbi_uc const *buffer, int len) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__is_16_main(&s); +} + +STBIDEF int stbi_is_16_bit_from_callbacks(stbi_io_callbacks const *c, void *user) +{ + stbi__context s; + stbi__start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi__is_16_main(&s); +} + +#endif // STB_IMAGE_IMPLEMENTATION + +/* + revision history: + 2.20 (2019-02-07) support utf8 filenames in Windows; fix warnings and platform ifdefs + 2.19 (2018-02-11) fix warning + 2.18 (2018-01-30) fix warnings + 2.17 (2018-01-29) change sbti__shiftsigned to avoid clang -O2 bug + 1-bit BMP + *_is_16_bit api + avoid warnings + 2.16 (2017-07-23) all functions have 16-bit variants; + STBI_NO_STDIO works again; + compilation fixes; + fix rounding in unpremultiply; + optimize vertical flip; + disable raw_len validation; + documentation fixes + 2.15 (2017-03-18) fix png-1,2,4 bug; now all Imagenet JPGs decode; + warning fixes; disable run-time SSE detection on gcc; + uniform handling of optional "return" values; + thread-safe initialization of zlib tables + 2.14 (2017-03-03) remove deprecated STBI_JPEG_OLD; fixes for Imagenet JPGs + 2.13 (2016-11-29) add 16-bit API, only supported for PNG right now + 2.12 (2016-04-02) fix typo in 2.11 PSD fix that caused crashes + 2.11 (2016-04-02) allocate large structures on the stack + remove white matting for transparent PSD + fix reported channel count for PNG & BMP + re-enable SSE2 in non-gcc 64-bit + support RGB-formatted JPEG + read 16-bit PNGs (only as 8-bit) + 2.10 (2016-01-22) avoid warning introduced in 2.09 by STBI_REALLOC_SIZED + 2.09 (2016-01-16) allow comments in PNM files + 16-bit-per-pixel TGA (not bit-per-component) + info() for TGA could break due to .hdr handling + info() for BMP to shares code instead of sloppy parse + can use STBI_REALLOC_SIZED if allocator doesn't support realloc + code cleanup + 2.08 (2015-09-13) fix to 2.07 cleanup, reading RGB PSD as RGBA + 2.07 (2015-09-13) fix compiler warnings + partial animated GIF support + limited 16-bpc PSD support + #ifdef unused functions + bug with < 92 byte PIC,PNM,HDR,TGA + 2.06 (2015-04-19) fix bug where PSD returns wrong '*comp' value + 2.05 (2015-04-19) fix bug in progressive JPEG handling, fix warning + 2.04 (2015-04-15) try to re-enable SIMD on MinGW 64-bit + 2.03 (2015-04-12) extra corruption checking (mmozeiko) + stbi_set_flip_vertically_on_load (nguillemot) + fix NEON support; fix mingw support + 2.02 (2015-01-19) fix incorrect assert, fix warning + 2.01 (2015-01-17) fix various warnings; suppress SIMD on gcc 32-bit without -msse2 + 2.00b (2014-12-25) fix STBI_MALLOC in progressive JPEG + 2.00 (2014-12-25) optimize JPG, including x86 SSE2 & NEON SIMD (ryg) + progressive JPEG (stb) + PGM/PPM support (Ken Miller) + STBI_MALLOC,STBI_REALLOC,STBI_FREE + GIF bugfix -- seemingly never worked + STBI_NO_*, STBI_ONLY_* + 1.48 (2014-12-14) fix incorrectly-named assert() + 1.47 (2014-12-14) 1/2/4-bit PNG support, both direct and paletted (Omar Cornut & stb) + optimize PNG (ryg) + fix bug in interlaced PNG with user-specified channel count (stb) + 1.46 (2014-08-26) + fix broken tRNS chunk (colorkey-style transparency) in non-paletted PNG + 1.45 (2014-08-16) + fix MSVC-ARM internal compiler error by wrapping malloc + 1.44 (2014-08-07) + various warning fixes from Ronny Chevalier + 1.43 (2014-07-15) + fix MSVC-only compiler problem in code changed in 1.42 + 1.42 (2014-07-09) + don't define _CRT_SECURE_NO_WARNINGS (affects user code) + fixes to stbi__cleanup_jpeg path + added STBI_ASSERT to avoid requiring assert.h + 1.41 (2014-06-25) + fix search&replace from 1.36 that messed up comments/error messages + 1.40 (2014-06-22) + fix gcc struct-initialization warning + 1.39 (2014-06-15) + fix to TGA optimization when req_comp != number of components in TGA; + fix to GIF loading because BMP wasn't rewinding (whoops, no GIFs in my test suite) + add support for BMP version 5 (more ignored fields) + 1.38 (2014-06-06) + suppress MSVC warnings on integer casts truncating values + fix accidental rename of 'skip' field of I/O + 1.37 (2014-06-04) + remove duplicate typedef + 1.36 (2014-06-03) + convert to header file single-file library + if de-iphone isn't set, load iphone images color-swapped instead of returning NULL + 1.35 (2014-05-27) + various warnings + fix broken STBI_SIMD path + fix bug where stbi_load_from_file no longer left file pointer in correct place + fix broken non-easy path for 32-bit BMP (possibly never used) + TGA optimization by Arseny Kapoulkine + 1.34 (unknown) + use STBI_NOTUSED in stbi__resample_row_generic(), fix one more leak in tga failure case + 1.33 (2011-07-14) + make stbi_is_hdr work in STBI_NO_HDR (as specified), minor compiler-friendly improvements + 1.32 (2011-07-13) + support for "info" function for all supported filetypes (SpartanJ) + 1.31 (2011-06-20) + a few more leak fixes, bug in PNG handling (SpartanJ) + 1.30 (2011-06-11) + added ability to load files via callbacks to accomidate custom input streams (Ben Wenger) + removed deprecated format-specific test/load functions + removed support for installable file formats (stbi_loader) -- would have been broken for IO callbacks anyway + error cases in bmp and tga give messages and don't leak (Raymond Barbiero, grisha) + fix inefficiency in decoding 32-bit BMP (David Woo) + 1.29 (2010-08-16) + various warning fixes from Aurelien Pocheville + 1.28 (2010-08-01) + fix bug in GIF palette transparency (SpartanJ) + 1.27 (2010-08-01) + cast-to-stbi_uc to fix warnings + 1.26 (2010-07-24) + fix bug in file buffering for PNG reported by SpartanJ + 1.25 (2010-07-17) + refix trans_data warning (Won Chun) + 1.24 (2010-07-12) + perf improvements reading from files on platforms with lock-heavy fgetc() + minor perf improvements for jpeg + deprecated type-specific functions so we'll get feedback if they're needed + attempt to fix trans_data warning (Won Chun) + 1.23 fixed bug in iPhone support + 1.22 (2010-07-10) + removed image *writing* support + stbi_info support from Jetro Lauha + GIF support from Jean-Marc Lienher + iPhone PNG-extensions from James Brown + warning-fixes from Nicolas Schulz and Janez Zemva (i.stbi__err. Janez (U+017D)emva) + 1.21 fix use of 'stbi_uc' in header (reported by jon blow) + 1.20 added support for Softimage PIC, by Tom Seddon + 1.19 bug in interlaced PNG corruption check (found by ryg) + 1.18 (2008-08-02) + fix a threading bug (local mutable static) + 1.17 support interlaced PNG + 1.16 major bugfix - stbi__convert_format converted one too many pixels + 1.15 initialize some fields for thread safety + 1.14 fix threadsafe conversion bug + header-file-only version (#define STBI_HEADER_FILE_ONLY before including) + 1.13 threadsafe + 1.12 const qualifiers in the API + 1.11 Support installable IDCT, colorspace conversion routines + 1.10 Fixes for 64-bit (don't use "unsigned long") + optimized upsampling by Fabian "ryg" Giesen + 1.09 Fix format-conversion for PSD code (bad global variables!) + 1.08 Thatcher Ulrich's PSD code integrated by Nicolas Schulz + 1.07 attempt to fix C++ warning/errors again + 1.06 attempt to fix C++ warning/errors again + 1.05 fix TGA loading to return correct *comp and use good luminance calc + 1.04 default float alpha is 1, not 255; use 'void *' for stbi_image_free + 1.03 bugfixes to STBI_NO_STDIO, STBI_NO_HDR + 1.02 support for (subset of) HDR files, float interface for preferred access to them + 1.01 fix bug: possible bug in handling right-side up bmps... not sure + fix bug: the stbi__bmp_load() and stbi__tga_load() functions didn't work at all + 1.00 interface to zlib that skips zlib header + 0.99 correct handling of alpha in palette + 0.98 TGA loader by lonesock; dynamically add loaders (untested) + 0.97 jpeg errors on too large a file; also catch another malloc failure + 0.96 fix detection of invalid v value - particleman@mollyrocket forum + 0.95 during header scan, seek to markers in case of padding + 0.94 STBI_NO_STDIO to disable stdio usage; rename all #defines the same + 0.93 handle jpegtran output; verbose errors + 0.92 read 4,8,16,24,32-bit BMP files of several formats + 0.91 output 24-bit Windows 3.0 BMP files + 0.90 fix a few more warnings; bump version number to approach 1.0 + 0.61 bugfixes due to Marc LeBlanc, Christopher Lloyd + 0.60 fix compiling as c++ + 0.59 fix warnings: merge Dave Moore's -Wall fixes + 0.58 fix bug: zlib uncompressed mode len/nlen was wrong endian + 0.57 fix bug: jpg last huffman symbol before marker was >9 bits but less than 16 available + 0.56 fix bug: zlib uncompressed mode len vs. nlen + 0.55 fix bug: restart_interval not initialized to 0 + 0.54 allow NULL for 'int *comp' + 0.53 fix bug in png 3->4; speedup png decoding + 0.52 png handles req_comp=3,4 directly; minor cleanup; jpeg comments + 0.51 obey req_comp requests, 1-component jpegs return as 1-component, + on 'test' only check type, not whether we support this variant + 0.50 (2006-11-19) + first released version +*/ + + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/sdk/test_host_stub.c b/sdk/test_host_stub.c new file mode 100644 index 0000000..da41ebf --- /dev/null +++ b/sdk/test_host_stub.c @@ -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 + +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; } diff --git a/sdk/test_ime_fix b/sdk/test_ime_fix new file mode 100755 index 0000000..6d6f73b Binary files /dev/null and b/sdk/test_ime_fix differ diff --git a/sdk/test_ime_fix.c b/sdk/test_ime_fix.c new file mode 100644 index 0000000..d9034b4 --- /dev/null +++ b/sdk/test_ime_fix.c @@ -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 +#include +#include +#include +#include +#include + +/* 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; +} diff --git a/sdk/test_meow_ui_config.c b/sdk/test_meow_ui_config.c new file mode 100644 index 0000000..ff6b733 --- /dev/null +++ b/sdk/test_meow_ui_config.c @@ -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=,验证持久目录读写 + * ./test_meow_ui_config tmp → unsetenv MEOW_DATA_DIR,验证 /tmp/meowcfg_ 回退 + * + * 任一断言失败 → 打印 FAIL 并以非零退出码结束。 + */ +#include "meow_ui.h" + +#include +#include +#include +#include + +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_"); + 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; +} diff --git a/sdk/test_meow_ui_theme b/sdk/test_meow_ui_theme new file mode 100755 index 0000000..d24f89c Binary files /dev/null and b/sdk/test_meow_ui_theme differ diff --git a/sdk/test_meow_ui_theme.c b/sdk/test_meow_ui_theme.c new file mode 100644 index 0000000..6a11280 --- /dev/null +++ b/sdk/test_meow_ui_theme.c @@ -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 +#include +#include +#include + +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