MeowOS for HiBy R3 Pro II — 公共发布版本(历史已重置)

• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC)
• 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库)
• 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo
增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
This commit is contained in:
MeowOS Dev committed 2026-08-30 01:12:34 +08:00
commit c3f70ed893
15 files changed
+10906

No files matched your search

Executable
+29
View File
@@ -0,0 +1,29 @@
#!/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
OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_ui_sys stb_image"
for f in $OBJS; do
echo "== cc $f =="
"$CC" -Os -static -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
+315
View File
@@ -0,0 +1,315 @@
/*
* 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 文件)使用。
*/
#ifndef MEOW_UI_H
#define MEOW_UI_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ============================================================ */
/* 物理键 keycode(内核 input.h 常用值;KEEP 与 launcher/settings 一致) */
/* ============================================================ */
#define MEOW_KEY_POWER 116
#define MEOW_KEY_VOLUMEDOWN 114
#define MEOW_KEY_VOLUMEUP 115
#define MEOW_KEY_PLAYPAUSE 164
#define MEOW_KEY_PREVIOUS 19 /* KEY_PREVIOUSSONG */
#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
/* ============================================================ */
/* §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 */
/* ============================================================ */
/* §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 */
/* ============================================================ */
/* §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.1 输入法(IME)—— 内置触屏软键盘(仅英文 QWERTY / 数字,ASCII) */
/* ============================================================ */
typedef enum { MEOW_IME_ASCII, MEOW_IME_NUM } 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;
/* 阻塞:弹出键盘直到确认/取消。返回 1=确认(buf 已回填),0=取消。 */
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);
/* ============================================================ */
/* §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 */
} 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);
/* ============================================================ */
/* §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);
/* ============================================================ */
/* §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);
/* ============================================================ */
/* §8 主题/配置 */
/* ============================================================ */
typedef enum { MEOW_THEME_LIGHT, MEOW_THEME_DARK, MEOW_THEME_AUTO } meow_theme_mode;
typedef struct meow_theme {
uint32_t bg; /* 背景色 */
uint32_t fg; /* 前景/正文 */
uint32_t accent; /* 强调色(按钮/选中) */
uint32_t dim; /* 次要文字 */
int font_title; /* 28 */
int font_body; /* 20 */
} 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);
/* ============================================================ */
/* §9 应用日志(统一日志 API,M2.51 最小可用版) */
/* ============================================================ */
/* app 只需一行 meow_log();SDK 管「写哪/怎么写/过滤/降级/上限」:
* - 路径:<MEOW_LOG_DIR>/apps/<MEOW_LOG_APP>/logs/<启动时间>_<pid>.log
* (目录不存在 mkdir -p,按 app 分目录),每次启动=新文件,同一次启动内 O_APPEND 追加;
* - 级别过滤:lvl < meow_log_get_level() 丢弃;默认 MEOW_LOG_INFO;
* - 格式:<monotonic_ms> [<app_id>] <LEVEL> message\n;
* - 上限:单文件 ≥2MB 自动开新文件追加 _<n>;每 App 总量 ≥50MB 按期删最旧,防撑爆卡;
* - 无卡/写失败降级 stderr,不阻塞 app 不崩;线程安全。
* 注意:MEOW_LOG_APP 由 launcher 注入;未注入时默认 app_id="app"。 */
typedef enum { MEOW_LOG_DEBUG=0, MEOW_LOG_INFO, MEOW_LOG_WARN, MEOW_LOG_ERROR } meow_log_level;
void meow_log(meow_log_level lvl, const char *fmt, ...); /* app 一行调用,写到按 app 分离日志 */
void meow_log_set_level(meow_log_level lvl); /* 可选:app 设本 app 级别;默认 MEOW_LOG_INFO */
meow_log_level meow_log_get_level(void);
/* ============================================================ */
/* §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 只需保证 title/body/target_activity 指针在调用返回前有效)。 */
typedef struct meow_notify {
meow_notify_category cat; /* 类型(未声明则按 4 种上限自动注册) */
meow_notify_urgency urgency; /* 紧急度 */
const char *title; /* summary:标题,单行(必填,NULL 则发送失败) */
const char *body; /* body:正文,可多行(可 NULL) */
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);
/* 查询当前活动(未过期、未取消)通知数/是否还有。 */
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_H */
+650
View File
@@ -0,0 +1,650 @@
/*
* 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 "../meow_launcherd/meow_font.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
/* 本设备实测 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/MeowAPPs/fonts/font.png"
#define MEOW_FONT_IDX_CARD "/usr/data/mnt/sd_0/MeowAPPs/fonts/font.idx"
#define MEOW_FONT_PNG_SYS "/usr/share/meowos/font/font.png"
#define MEOW_FONT_IDX_SYS "/usr/share/meowos/font/font.idx"
#define MUI_DEFAULT_W 480
#define MUI_DEFAULT_H 720
/* 脏矩形上限(超出合并为全屏) */
#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;
static char *g_dump_path = NULL;
/* ---- 帧率调度(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;
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(); }
/* launcher(系统组件)与 SDK 共用同一 back_buf:launcher 的 fb 绘制指针指向它,
* 从而 launcher 与 app 统一渲染栈(同一离屏 + 同一 present)。 */
unsigned char *meow_ui_back_buf(void) { return g_back_buf; }
/* ================= 像素写入 ================= */
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 (x < 0 || x >= g_xres || y < 0 || y >= g_yres) 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 (x < 0 || x >= g_xres || y < 0 || y >= g_yres) 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;
/* 快速整刷(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;
int x0 = x, y0 = y;
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_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);
}
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;
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; /* 上次画过的条长(清条时只清到它,避免残留) */
void meow_ui_swipe_hint(int x)
{
if (!g_init_ok) return;
const meow_theme *th = meow_theme_get();
/* 进度条画在状态栏下沿(z 在状态栏之下,不被全局状态栏覆盖)。
* 有状态栏(非全屏)→ y = 状态栏高度;无状态栏(全屏/隐藏)→ y=0。 */
int hy = meow_ui_statusbar_height();
if (x > 0) {
if (x > g_xres) x = g_xres;
/* 往回滑(新长度 < 上次)时,先清掉「上次画到旧长度、但本次超出的部分」,
* 否则条只增不减(用户要求:右滑变长、左滑回缩实时可见)。 */
if (g_hint_last > x) meow_ui_fill_rect(0, hy, g_hint_last, MUI_HINT_H, th->bg);
meow_ui_fill_rect(0, hy, x, MUI_HINT_H, th->accent);
g_hint_last = x;
} else if (g_hint_last > 0) {
/* 手指回原点/抬手未达标/抬手达标:清掉此前画的条(填回背景色) */
meow_ui_fill_rect(0, hy, g_hint_last, MUI_HINT_H, th->bg);
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) {
meow_font_drawtext((uint16_t *)g_fb, x, y, s, rgb565(color), size);
int w = meow_font_width_text(s, size);
int hh = meow_font_line_height(size);
dirty_add(x, y, w, hh);
} else {
int sc = fallback_scale(size);
draw_text5x7(x, y, s, sc, color);
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);
}
/* 顶部状态栏条是否被本次脏区触及(含整帧):触及或数据已变(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;
}
static void present(void)
{
if (!g_init_ok || !g_back_buf) return;
if (!(g_dirty_full || g_ndirty > 0)) 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;
}
/* 全局状态栏(A-0):上屏前把状态栏画进 back_buf 顶部 MUI_SBAR_H 行(z 最顶)。
* 全屏/隐藏时 active()==0 不画(沉浸),app 内容可占满整屏。触发条件:
* 数据已变(1s 轻刷/显隐切换) 或 顶条被 app 内容触及(含整帧清+绘,未 inset 时被顶条覆盖)。
* 无需每帧重画——app 局部刷新不触及顶条时,back_buf/screen 顶条保持不变,省一次 blit。 */
if (meow_ui_sys_statusbar_active() &&
(meow_ui_sys_statusbar_force() || statusbar_touched()))
meow_ui_sys_statusbar_render();
if (g_fb_mem) {
ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */
unsigned long long tbg = now_ms();
if (g_dirty_full) {
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];
for (int yy = 0; yy < g_dirty[i][3]; 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) { present(); }
void meow_ui_flush_region(int x, int y, int w, int h)
{ dirty_add(x, y, w, h); 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; }
/* ================= 初始化/销毁 ================= */
static int load_font(void)
{
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;
}
int meow_ui_init(const meow_ui_opts *opts)
{
if (g_init_ok) return 0;
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();
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/蓝牙/音量),状态栏就绪 */
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; }
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; }
+235
View File
@@ -0,0 +1,235 @@
/*
* meow_ui_ime.c — 内置触屏软键盘(B1.6)。仅英文 QWERTY / 数字,ASCII。
*
* 阻塞:meow_ui_text_input(ctx) 弹出键盘,自绘自分发直到确认(enter)/取消(esc|背景)。
* 非阻塞:meow_ui_text_input_async(ctx) 打开后返回;meow_ui_run 在 on_tick 间隙
* 随帧绘制并拦截触摸,app 用 meow_ime_active()/meow_ime_text() 轮询。
*
* 键盘覆盖屏幕底部约 300px;点按输入;键位 44×36(touch target 达标)。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#define IME_MAXKEYS 96
#define KB_TOP 420 /* 面板顶(预览行起点) */
#define GW 44 /* 键宽 */
#define GH 36 /* 键高 */
#define GAP 4 /* 键间距 */
#define GCOL 48 /* 列步进 = GW+GAP */
typedef enum { K_CH, K_SHIFT, K_SPACE, K_BACK, K_ENTER, K_ESC } 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;
/* ---- 布局 ---- */
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_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 */
add_ch_row(y, "1234567890");
add_ch_row(y + GCOL, "-/.:*#+=");
{
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
add_key(x, y + GCOL * 2, GW, GH, K_ESC, 'X');
add_key(x + GCOL, y + GCOL * 2, (GW + GAP) * 7, GH, K_SPACE, ' ');
add_key(x + GCOL * 9, y + GCOL * 2, GW, GH, K_BACK, '<');
add_key(x + GCOL * 9, y + GCOL * 2, GW, GH, K_ENTER, '>'); /* 覆盖:回车 */
}
/* 单独一行回车 */
{
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
add_key(x + GCOL * 4, y + GCOL * 3, 2 * GW + GAP, GH, K_ENTER, '>');
}
}
}
/* ---- 编辑动作 ---- */
static int is_letter(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); }
static void apply_key(const struct kbkey *k)
{
switch (k->kind) {
case K_CH: {
char c = g_shift && is_letter(k->ch) ? (char)toupper((unsigned char)k->ch) : k->ch;
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; g_ime_done = 1; break;
case K_ESC: g_ime_result = 0; g_ime_done = 1; break;
}
}
/* ---- 绘制 ---- */
void meow_ime_draw(void)
{
if (!g_ime_active) return;
const meow_theme *th = meow_theme_get();
/* 面板 */
meow_ui_fill_rect(0, KB_TOP, 480, 720 - KB_TOP, 0x1C1C22);
/* 预览行 */
meow_ui_fill_rect(0, KB_TOP, 480, 42, 0x2A2A33);
char preview[256];
snprintf(preview, sizeof preview, "%s_", g_ime_buf);
meow_ui_text(14, KB_TOP + 9, preview, th->font_body, 0xFFFFFF);
/* 键 */
for (int i = 0; i < g_nkeys; i++) {
const struct kbkey *k = &g_keys[i];
int is_fn = (k->kind != K_CH);
uint32_t bg = is_fn ? 0x33333C : 0xFFFFFF;
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);
uint32_t fg = is_fn ? 0xFFFFFF : 0x1A1A1E;
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 { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, label, 20, fg); }
}
}
/* ---- 命中 ---- */
int meow_ime_handle_tap(int x, int y)
{
if (!g_ime_active || g_ime_done) return 0;
if (y < KB_TOP) { g_ime_result = 0; g_ime_done = 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; }
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;
}
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;
build_kb(ctx->mode);
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()) {
meow_input_ev e;
while (meow_input_pop(&e)) {
if (e.type == MEOW_EV_TAP) meow_ime_handle_tap(e.a, e.b);
}
meow_ime_draw();
meow_ui_flush();
usleep(16000);
}
int result = g_ime_result;
meow_ime_close(result);
return result;
}
+126
View File
@@ -0,0 +1,126 @@
/*
* meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入),
* 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "stb_image.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct meow_img {
int w, h;
unsigned char *rgba; /* 4 通道 straight alpha */
};
static meow_img *img_alloc(int w, int h, unsigned char *rgba)
{
if (w <= 0 || h <= 0 || !rgba) { if (rgba) stbi_image_free(rgba); return NULL; }
meow_img *im = (meow_img *)malloc(sizeof *im);
if (!im) { stbi_image_free(rgba); return NULL; }
im->w = w; im->h = h; im->rgba = rgba;
return im;
}
meow_img *meow_img_load_mem(const void *buf, int len)
{
if (!buf || len <= 0) return NULL;
int w = 0, h = 0, c = 0;
unsigned char *p = stbi_load_from_memory((const unsigned char *)buf, len, &w, &h, &c, 4);
if (!p) return NULL;
return img_alloc(w, h, p);
}
meow_img *meow_img_load_file(const char *path)
{
if (!path) return NULL;
int w = 0, h = 0, c = 0;
unsigned char *p = stbi_load(path, &w, &h, &c, 4);
if (!p) return NULL;
return img_alloc(w, h, p);
}
void meow_img_free(meow_img *im)
{
if (!im) return;
if (im->rgba) stbi_image_free(im->rgba);
free(im);
}
int meow_img_w(meow_img *im) { return im ? im->w : 0; }
int meow_img_h(meow_img *im) { return im ? im->h : 0; }
/* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
{
if (!im || dw <= 0 || dh <= 0) return NULL;
unsigned char *out = (unsigned char *)malloc((size_t)dw * dh * 4);
if (!out) return NULL;
float s = 1.0f;
float ox = 0.0f, oy = 0.0f;
if (cover) {
float sx = (float)im->w / dw, sy = (float)im->h / dh;
s = sx > sy ? sx : sy; /* 铺满 → 可能外溢,居中裁 */
float sw = im->w / s, sh = im->h / s; /* 目标显示寸(sw×sh) */
ox = (float)(dw - sw) * 0.5f;
oy = (float)(dh - sh) * 0.5f;
if (ox < 0) ox = 0; if (oy < 0) oy = 0;
for (int dy = 0; dy < dh; dy++) {
int syi = (int)((dy + oy) / s);
if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1;
for (int dx = 0; dx < dw; dx++) {
int sxi = (int)((dx + ox) / s);
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);
}
+333
View File
@@ -0,0 +1,333 @@
/*
* 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。
* 物理键:EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.24 全面按键接管:电源/音量/播放/
* 上下曲全部由系统(watchdog/launcher)消费,SDK 吞噬不发给 APP(应用只收 tap/swipe/tick)。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "meow_ui_sys.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <poll.h>
#include <pthread.h>
/* 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
#define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */
#define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */
#define QUEUE_N 128
#define MAX_DEVS 8
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;
}
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; /* 原点是否已锁定 */
/* 按下边沿:只标记按下,绝不用传入坐标立即锁原点(避免用上一手势残留坐标当原点)。
* 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;
}
/* 由 ABS_X/ABS_Y 驱动:若本次按下且原点未锁且坐标有效 → 锁原点;再更新当前坐标。 */
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;
}
if (x >= 0) t_cx = x;
if (y >= 0) t_cy = y;
}
static void touch_release(void)
{
if (!t_down) return;
t_down = 0; t_press_valid = 0;
/* 本次按下从未锁到有效坐标(幽灵触摸/坐标未就绪):丢弃,且清空全部轨迹。 */
if (!t_origin_valid) { t_cx = t_cy = -1; t_dx = t_dy = 0; 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;
/* 手势判定只在「抬手(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) {
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;
}
/* ---- 输入线程:poll 所有 event 设备 + 解析 ---- */
static void *input_thread(void *arg)
{
(void)arg;
struct pollfd fds[MAX_DEVS];
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; nfd++; }
}
while (g_thread_run) {
if (nfd == 0) { usleep(20000); continue; }
int r = poll(fds, nfd, 20);
if (r < 0) { if (errno == EINTR) continue; usleep(20000); continue; }
if (r == 0) continue;
for (int i = 0; i < nfd; i++) {
if (!(fds[i].revents & POLLIN)) continue;
unsigned char ev[16];
while (read(fds[i].fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8);
uint16_t code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) {
if (t_down) touch_update(val, t_cy); /* 锁原点+更新当前(t_down 时才驱动) */
else t_cx = val;
/* 左滑进度实时上报(视觉条):原点确实在左缘(t_dx<EDGE_W) 且 向右移动
* (t_cx>t_dx)。抬手触发仍由 touch_release 判定;这里只报过程。
* 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 || code == MEOW_KEY_VOLUMEDOWN ||
code == MEOW_KEY_VOLUMEUP || code == MEOW_KEY_PLAYPAUSE ||
code == MEOW_KEY_PREVIOUS || code == MEOW_KEY_NEXT || code == 165) {
/* M2.22(A)/M2.24 纪律:物理键(电源 116 / 音量 114,115 / 播放 164 /
* 上一曲 19,165 / 下一曲 163)全部是系统语义 —— watchdog/launcher 统一消费,
* 绝不下发 APP。应用不碰物理键、不把任何物理键映射成确认/exit/返回/翻页
* (KEY_CONTROL_DESIGN §1.3 / APPS.md §3.4.1);APP 只接收 tap/swipe/tick。
* 忽略该键,不发 MEOW_EV_KEY。 */
} else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */
push_ev(MEOW_EV_KEY, code, 1);
} else if (val == 0) { /* 抬起 */
push_ev(MEOW_EV_KEY, code, 0);
}
}
}
}
}
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; }
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) {
meow_input_ev e;
int got = 0;
while (meow_input_pop(&e)) {
got = 1;
int in_startup = (now_ms() < startup_cooldown_until); /* M2.22(C) */
if (e.type == MEOW_EV_TAP) {
/* 抬手未达标:清视觉条 */
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_tap(e.a, e.b)) continue;
if (g_events_set && g_events.on_tap) g_events.on_tap(e.a, e.b);
} else if (e.type == MEOW_EV_KEY) {
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);
/* 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_SWIPE_PROGRESS) {
if (in_startup) continue; /* 启动窗口进度条静音 */
int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */
/* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。
* 全屏/沉浸不画系统条(无返回手势);APP 若注册 on_swipe_progress 仍可自绘。 */
if (meow_ui_sys_statusbar_active()) 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);
}
}
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)也保持时钟走动。 */
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_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);
}
}
}
+65
View File
@@ -0,0 +1,65 @@
/*
* meow_ui_internal.h — SDK 内部各 TU 共享声明(不随 meow_ui.h 发布给应用)。
* 仅供 meow_ui_*.c 之间协作:渲染后端底层能力 + 输入事件队列 + 主题取用。
*/
#ifndef MEOW_UI_INTERNAL_H
#define MEOW_UI_INTERNAL_H
#include <stdint.h>
#include "meow_ui.h"
/* ---- 渲染后端(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);
/* 标记整帧脏(launcher 手动脏模型桥接:自绘 raw 像素后标全屏,present 走 SDK)。 */
void meow_gfx_mark_full(void);
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
unsigned char *meow_ui_back_buf(void);
/* 实心圆角矩形(控件按钮底色用;公共 §3 只有空心 draw_rect_round)。[D3] */
void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color);
/* 是否运行于真实 fb 之上(0 = 头less/离屏,qemu/宿主)。 */
int meow_gfx_is_device(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=左缘向内划=返回手势) */
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);
/* ---- 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);
#endif /* MEOW_UI_INTERNAL_H */
+279
View File
@@ -0,0 +1,279 @@
/*
* 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(日志目录)、MEOW_LOG_APP(app_id,无则 "app")、
* MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写
* `<log_dir>/apps/<app_id>/logs/<启动时间>_<pid>[._<n>].log`(目录自动 mkdir -p),
* 按 app 分目录 + 每次启动新文件(不同启动=不同文件,同一次启动内 O_APPEND 追加),
* 级别过滤(lvl < level 丢弃);
* - 格式:`<monotonic_ms> [<app_id>] <LEVEL> message\n`(meow_ui_time_ms() 取毫秒);
* - 无 log_dir(无卡)/目录不可建/写失败 → 降级 stderr,不阻塞 app 不崩。
*
* 上限(用户定,防撑爆 TF 卡):
* - 单文件 ≥ MEOW_LOG_FILE_MAX(2MB)→ 关当前 fd,开新文件并追加序号 `_<n>`;
* - 每 App 日志总量 ≥ MEOW_LOG_TOTAL_MAX(50MB)→ 按期删最旧文件(按 mtime),
* 节流(每 MEOW_LOG_CLEANUP_EVERY 次写入检查一次,另在首次打开/每次轮转时检查),
* 避免高频日志时逐行 readdir/stat。
*
* 约定:与 meow_ui_gfx.c 同风格(M2.x 中文注释)。线程安全(pthread_mutex,launcher
* 当前台/后台进程都会在已链 -lpthread 的应用里调用)。
*/
#include "meow_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <time.h>
/* 单行消息上限(超长截断) */
#define MEOW_LOG_LINE_MAX 1024
/* 日志上限:单文件 2MB 开新文件;每 App 总量 50MB 删最旧 */
#define MEOW_LOG_FILE_MAX (2 * 1024 * 1024)
#define MEOW_LOG_TOTAL_MAX (50 * 1024 * 1024)
#define MEOW_LOG_CLEANUP_MAX 128 /* 一次清理最多扫描文件数 */
#define MEOW_LOG_CLEANUP_EVERY 2048 /* 每 N 次写入做一次总量清理(节流) */
/* 级别字符串表(与 meow_log_level 序号对齐) */
static const char *g_lvl_str[] = { "DEBUG", "INFO", "WARN", "ERROR" };
/* 内部状态(各字段约定见头注释) */
static pthread_mutex_t g_log_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_once_t g_log_once = PTHREAD_ONCE_INIT;
static char g_app_id[64] = "app"; /* env MEOW_LOG_APP,无则 "app" */
static char g_log_dir[256] = ""; /* env MEOW_LOG_DIR,无/空则降级 stdio */
static meow_log_level g_level = MEOW_LOG_INFO; /* 默认 INFO */
/* M2.52:日志文件为 `<log_dir>/apps/<app_id>/logs/<ts>_<pid>[._<n>].log`。
* g_log_base 存不含序号后缀的路径前缀 `<dir>/apps/<app>/logs/<ts>_<pid>`;
* g_log_seq 为轮转序号(0=首个文件无后缀;>0 追加 `_<n>`)。两者均在 log_init()
* (pthread_once 那次)里生成一次。 */
static char g_log_base[384] = "";
static char g_log_file[512] = "";
static int g_log_seq = 0;
static int g_log_fd = -1; /* 当前日志 fd;-1=未开 */
static int g_log_fallback = 0; /* 1=无卡/写失败,降级 stderr */
static int g_log_open_tried = 0; /* 是否已尝试开首个文件(只试一次,失败则降级) */
static unsigned long g_write_count = 0; /* 累计写入次数(周期总量清理节流) */
/* 由 g_log_base + g_log_seq 重建当前文件路径(seq=0 无后缀,>0 追加 _<n>)。 */
static void log_rebuild_file(void)
{
if (g_log_seq <= 0)
snprintf(g_log_file, sizeof g_log_file, "%s.log", g_log_base);
else
snprintf(g_log_file, sizeof g_log_file, "%s_%d.log", g_log_base, g_log_seq);
}
/* 一次性初始化:读 env 决定 app_id/log_dir/level;构建 g_log_base 与首个 g_log_file。
* (pthread_once 保证仅一次。) */
static void log_init(void)
{
const char *s;
/* app_id:env MEOW_LOG_APP,无则 "app" */
s = getenv("MEOW_LOG_APP");
if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s);
/* log_dir:env MEOW_LOG_DIR,无/空则 ""(降级 stdio) */
s = getenv("MEOW_LOG_DIR");
if (s && s[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", s);
/* M2.52:构建完整前缀 `<log_dir>/apps/<app_id>/logs/<ts>_<pid>`。
* ts 用 localtime 的 %Y%m%d_%H%M%S(启动时刻);尾拼 _<pid> 防同秒重复启动撞名,
* 从而每次启动=新文件,同一次启动内 O_APPEND 追加。若 log_dir 为空(无卡)
* 则跳过(log_open 降级 stderr)。 */
if (g_log_dir[0]) {
char ts[32];
time_t now = time(NULL);
struct tm tmnow;
localtime_r(&now, &tmnow);
strftime(ts, sizeof ts, "%Y%m%d_%H%M%S", &tmnow);
snprintf(g_log_base, sizeof g_log_base, "%s/apps/%s/logs/%s_%d",
g_log_dir, g_app_id, 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;
}
}
/* mkdir -p:逐分量 mkdir(已存在 EEXIST 视为成功)。返回 0=成功,-1=失败。 */
static int log_mkdir_p(const char *path)
{
char tmp[256];
size_t len, i;
if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1;
len = strlen(path);
memcpy(tmp, path, len + 1);
if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; /* 去尾斜杠 */
for (i = 1; i <= strlen(tmp); i++) {
if (tmp[i] == '/' || tmp[i] == '\0') {
char ch = tmp[i];
tmp[i] = '\0';
if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1;
tmp[i] = ch;
}
}
return 0;
}
/* M2.52:对 `<log_dir>/apps/<app_id>/logs` 增量 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/apps/%s/logs", g_log_dir, g_app_id) >= (int)sizeof logs) {
g_log_fallback = 1; return;
}
if (log_mkdir_p(logs) != 0) { g_log_fallback = 1; return; }
g_log_fd = open(g_log_file, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (g_log_fd < 0) g_log_fallback = 1;
}
/* 清理用文件记录(按 mtime 升序删最旧) */
typedef struct { char name[384]; time_t mtime; long size; } log_spill_t;
static int log_spill_cmp(const void *a, const void *b)
{
const log_spill_t *fa = a, *fb = b;
if (fa->mtime < fb->mtime) return -1;
if (fa->mtime > fb->mtime) return 1;
return 0;
}
/* 每 App 总量清理:扫描 `<log_dir>/apps/<app_id>/logs/` 下所有 .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/apps/%s/logs", g_log_dir, g_app_id)
>= (int)sizeof logs) return;
d = opendir(logs);
if (!d) return;
while ((e = readdir(d)) != NULL) {
struct stat st;
size_t nl;
if (e->d_name[0] == '.') continue;
nl = strlen(e->d_name);
if (nl < 5 || strcmp(e->d_name + nl - 4, ".log") != 0) continue; /* 只看 .log */
if (snprintf(path, sizeof path, "%s/%s", logs, e->d_name) >= (int)sizeof path) continue;
if (strcmp(path, g_log_file) == 0) continue; /* 跳过当前正写文件,避免删 unlink 后仍写 */
if (stat(path, &st) != 0) continue;
if (!S_ISREG(st.st_mode)) continue;
if (n >= MEOW_LOG_CLEANUP_MAX) break;
snprintf(files[n].name, sizeof files[n].name, "%s", path);
files[n].mtime = st.st_mtime;
files[n].size = (long)st.st_size;
total += (long)st.st_size;
n++;
}
closedir(d);
if (total <= MEOW_LOG_TOTAL_MAX) return;
qsort(files, (size_t)n, sizeof files[0], log_spill_cmp);
for (i = 0; i < n && total > MEOW_LOG_TOTAL_MAX; i++) {
if (unlink(files[i].name) == 0) total -= files[i].size;
}
}
void meow_log(meow_log_level lvl, const char *fmt, ...)
{
char body[MEOW_LOG_LINE_MAX];
char line[MEOW_LOG_LINE_MAX + 96];
const char *lvl_s;
va_list ap;
int n;
pthread_once(&g_log_once, log_init);
pthread_mutex_lock(&g_log_mu);
if (lvl < g_level) { pthread_mutex_unlock(&g_log_mu); return; } /* 级别过滤 */
va_start(ap, fmt);
vsnprintf(body, sizeof body, fmt, ap);
va_end(ap);
body[sizeof body - 1] = '\0';
if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); }
if (g_log_fd >= 0 && g_write_count == 0) log_cleanup_old(); /* 首次打开即清一次 */
/* 级别名钳位(防 app 传越界枚举值触发 OOB 读) */
lvl_s = (lvl >= 0 && lvl <= MEOW_LOG_ERROR) ? g_lvl_str[lvl]
: g_lvl_str[MEOW_LOG_ERROR];
if (g_log_fd >= 0) {
struct stat fst;
/* 单文件超限 → 轮转开新文件(_<n> 递增);fstat 每次写前检查(日志低频,开销可忽略) */
if (fstat(g_log_fd, &fst) == 0 && fst.st_size >= (off_t)MEOW_LOG_FILE_MAX) {
close(g_log_fd);
g_log_fd = -1;
g_log_seq++;
log_rebuild_file();
log_open();
if (g_log_fd >= 0) log_cleanup_old(); /* 轮转后顺带清一次 */
}
if (g_log_fd >= 0) {
n = snprintf(line, sizeof line, "%llu [%s] %s %s\n",
(unsigned long long)meow_ui_time_ms(),
g_app_id, lvl_s, body);
if (n > 0) { ssize_t wr = write(g_log_fd, line, (size_t)n); (void)wr; }
}
/* 周期总量清理(节流,避免高频日志逐行 readdir/stat) */
if (++g_write_count % MEOW_LOG_CLEANUP_EVERY == 0) log_cleanup_old();
} else {
/* 降级 stderr(无卡/写失败):不阻塞 app,不崩 */
fprintf(stderr, "[%s] %s %s\n", g_app_id, lvl_s, body);
}
pthread_mutex_unlock(&g_log_mu);
}
void meow_log_set_level(meow_log_level lvl)
{
pthread_once(&g_log_once, log_init);
pthread_mutex_lock(&g_log_mu);
g_level = lvl;
pthread_mutex_unlock(&g_log_mu);
}
meow_log_level meow_log_get_level(void)
{
meow_log_level l;
pthread_once(&g_log_once, log_init);
pthread_mutex_lock(&g_log_mu);
l = g_level;
pthread_mutex_unlock(&g_log_mu);
return l;
}
+253
View File
@@ -0,0 +1,253 @@
/*
* 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),同样不自动过期。
* - 落点:本阶段内部维护活动通知表 + 用既有 meow_log() 打日志作为落点;真实投递到
* 通知中心 UI / D-Bus 是后续任务——meow_notify 结构已对齐 freedesktop 字段
* (summary/body/…)为此预留,但不写死后端。
*
* 线程安全:pthread_mutex(同 meow_ui_log.c,launcher 前后台进程都会在已链 -lpthread
* 的应用里调用)。不依赖 /dev/fb0、不依赖板子,可宿主编译。内部只复用
* meow_ui_time_ms()(单调毫秒)与 meow_log()(统一日志)两个既有 SDK 能力。
*/
#include "meow_ui.h"
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <pthread.h>
/* ---- 内部活动表尺寸(配额见 meow_ui.h) ---- */
#define MEOW_NOTIFY_MAX_ACTIVE (MEOW_NOTIFY_MAX_TYPES * MEOW_NOTIFY_MAX_PER_TYPE)
/* 活动通知槽(内部;标题/正文等复制副本,不持 app 外部指针,避免栈字符串悬垂) */
typedef struct {
uint32_t id; /* 唯一 id;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];
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;
}
}
}
/* 填充一个活动槽(改前须确认槽空闲或已被显式复用)。 */
static void notify_fill(notify_slot_t *s, const meow_notify *n, uint32_t id, uint64_t now)
{
s->id = id;
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 : "");
s->progress = (n->progress < -1) ? -1 : (n->progress > 100 ? 100 : n->progress);
}
/* 落点:本阶段用统一日志标出一条投递;真实后端(通知中心 UI / D-Bus)后续替换此处。 */
static void notify_deliver(const notify_slot_t *s, const char *ev)
{
meow_log(MEOW_LOG_INFO, "[notify] %s id=%u type=%s urgency=%s title=%s",
ev, (unsigned)s->id, meow_notify_category_str(s->cat),
meow_notify_urgency_str(s->urg), s->title);
}
/* ============================================================ */
/* 公共 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;
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; break; }
} else {
count = notify_count_cat(n->cat);
if (count >= MEOW_NOTIFY_MAX_PER_TYPE) {
/* 满则丢该类型最旧一条(替换) */
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) { g_slots[oldest_idx].active = 0; g_slots[oldest_idx].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);
notify_deliver(&g_slots[slot_idx], "send");
pthread_mutex_unlock(&g_mu);
return id;
}
int meow_notify_cancel(uint32_t id)
{
int i, rc = 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) {
g_slots[i].active = 0;
g_slots[i].id = 0;
rc = 1;
break;
}
}
pthread_mutex_unlock(&g_mu);
return rc;
}
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];
}
+231
View File
@@ -0,0 +1,231 @@
/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
#define VOL_DEFAULT 60
/* 蓝牙 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 刷新后置位,强制重画顶部条 */
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 写同文件)。 */
static int settings_vol(void)
{
FILE *f = fopen(MEOW_SETTINGS_FILE, "r");
if (!f) return VOL_DEFAULT;
int vol = VOL_DEFAULT;
char buf[128];
while (fgets(buf, sizeof buf, f)) {
if (!strncmp(buf, "audio.volume=", 13)) {
int v = atoi(buf + 13);
if (v >= 0 && v <= 100) vol = v;
break;
}
}
fclose(f);
return vol;
}
int meow_ui_sys_init(void)
{
memset(&g_sys, 0, sizeof g_sys);
g_sys.batt = -1;
g_sys.vol = settings_vol();
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];
read_str("/sys/class/power_supply/battery/status", st, sizeof st);
g_sys.charging = (strstr(st, "Charg") || strstr(st, "Full")) ? 1 : 0;
char op[16];
read_str("/sys/class/net/wlan0/operstate", op, sizeof op);
g_sys.wifi_on = !strcmp(op, "up") ? 1 : 0;
g_sys.vol = settings_vol();
/* 蓝牙: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; }
/* ---- 公共 API ---- */
void meow_ui_set_statusbar(int visible)
{
g_statusbar_on = visible ? 1 : 0;
g_statusbar_force = 1; /* 显隐切换也要重画顶条 */
}
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; /* 进出全屏立即反映(显示/隐藏顶条) */
}
int meow_ui_statusbar_height(void)
{
return meow_ui_sys_statusbar_active() ? MUI_SBAR_H : 0;
}
/* ---- 内部(gfx/input 用) ---- */
int meow_ui_sys_statusbar_active(void)
{
return g_statusbar_on && !g_fullscreen;
}
int meow_ui_sys_statusbar_height(void)
{
return meow_ui_sys_statusbar_active() ? MUI_SBAR_H : 0;
}
int meow_ui_sys_statusbar_force(void)
{
int f = g_statusbar_force;
g_statusbar_force = 0;
return f;
}
/* 状态栏右侧信息串:batt% WIFI 蓝牙/蓝牙 VOL nn%(同 launcher statusbar_right_str) */
static void statusbar_right_str(char *out, size_t n)
{
char batt[16] = "", wifi[16] = "", vol[20] = "", bt[16] = "";
if (g_sys.batt >= 0) snprintf(batt, sizeof batt, "%d%%", g_sys.batt);
if (g_sys.wifi_on) snprintf(wifi, sizeof wifi, "WIFI");
snprintf(vol, sizeof vol, "VOL %d%%", g_sys.vol);
if (g_sys.bt_seen && g_sys.bt_on)
snprintf(bt, sizeof bt, meow_gfx_mfont_ready() ? "蓝牙" : "BT");
out[0] = 0;
if (batt[0]) strcat(out, batt);
if (wifi[0]) { if (out[0]) strcat(out, " "); strcat(out, wifi); }
if (bt[0]) { if (out[0]) strcat(out, " "); strcat(out, bt); }
if (vol[0]) { if (out[0]) strcat(out, " "); strcat(out, vol); }
}
void meow_ui_sys_statusbar_render(void)
{
if (!meow_ui_sys_statusbar_active()) return;
const meow_theme *th = meow_theme_get();
int w = meow_ui_width();
int h = MUI_SBAR_H;
/* 整条清底:覆盖 app 可能画进顶行的内容(z 最顶) */
meow_ui_fill_rect(0, 0, w, h, th->bg);
/* 时钟(左):每秒现取 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, (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2, timebuf, MUI_SBAR_FONT, th->fg);
/* 右侧信息串:batt% WIFI 蓝牙 VOL nn% */
char right[96];
statusbar_right_str(right, sizeof right);
if (right[0])
meow_ui_text_right(w - 8, (h - meow_ui_text_height(MUI_SBAR_FONT)) / 2, right, MUI_SBAR_FONT, th->fg);
/* 分隔线(栏内最底 1px,主题次要色);内容 inset=statusbar_height()=40 从 y=40 起即干净 */
meow_ui_fill_rect(0, h - 1, w, 1, th->dim);
g_statusbar_force = 0;
}
+57
View File
@@ -0,0 +1,57 @@
/*
* 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,status}
* - WiFi /sys/class/net/wlan0/operstate
* - 蓝牙 实际 powered(hciconfig -a hci0 的 UP;spawn 贵 → 缓存 30s 刷一次)
* - 音量 /usr/data/meow/settings.conf 的 audio.volume(launcher 同文件)
* - 时钟 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; /* operstate==up */
int bt_seen; /* hci0 节点存在 */
int bt_on; /* 实际上电(hciconfig UP) */
int vol; /* 音量 0-100 */
} meow_sys_state;
/* 顶部状态栏几何(与 launcher SB_BAR_H=40 / SB_FONT=28 对齐,视觉统一)。 */
#define MUI_SBAR_H 40
#define MUI_SBAR_FONT 28
/* 首次全量读取;失败仍返回 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)。 */
int meow_ui_sys_statusbar_active(void);
/* 状态栏高度(px):active 时 40,否则 0(供 meow_ui_statusbar_height 用)。 */
int meow_ui_sys_statusbar_height(void);
/* 自上次渲染后状态栏数据是否已变(1s 轻刷/显隐切换置位);读取即清除。
* 渲染后端 present() 用它决定是否重画顶部条(配合脏区/整帧判断)。 */
int meow_ui_sys_statusbar_force(void);
/* 把状态栏画进 back_buf(顶部 MUI_SBAR_H 行)+ 标脏。调用方负责随后上屏。 */
void meow_ui_sys_statusbar_render(void);
#endif /* MEOW_UI_SYS_H */
+197
View File
@@ -0,0 +1,197 @@
/*
* 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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MEOW_SETTINGS_FILE "/usr/data/meow/settings.conf"
static meow_theme g_light = {
0xE8EAF0, 0x1A1A2A, 0x2A5A8A, 0x4A4F5A, 28, 20
};
static meow_theme g_dark = {
0x141824, 0xFFFFFF, 0x4A6A9A, 0x9AA4C8, 28, 20
};
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 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" / 未知 → 深色 */
}
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);
}
/* 当前本地时刻的「当天第几分钟」(tm_hour*60+tm_min);供时段判定用(与 launcher s_now_min_of_day 一致)。 */
static int now_min_of_day(void)
{
time_t t = time(NULL);
struct tm tm;
localtime_r(&t, &tm);
return tm.tm_hour * 60 + tm.tm_min;
}
/* 深色时段判定(与 launcher theme_should_dark 完全一致):now_min ∈ [start,end) → 深色。
* - start==end 视为未配置,返回 -1(由调用方决定回退策略)。
* - start<end:非跨天(如 720→1080 = 12:00→18:00)。
* - start>end:跨午夜(如 1140→420 = 19:00→次日07:00)。
* - 边界:end 半开(恰好 end 分回到浅色),start 闭(恰好 start 分进入深色)。 */
static int theme_should_dark(int now_min, int start, int end)
{
if (start == end) return -1; /* 未配置 */
if (start < end)
return (now_min >= start && now_min < end) ? 1 : 0;
return (now_min >= start || now_min < end) ? 1 : 0; /* 跨午夜 */
}
static meow_theme_mode resolve_mode(meow_theme_mode m)
{
if (m != MEOW_THEME_AUTO) return m;
int d = theme_should_dark(now_min_of_day(), g_dark_start, g_dark_end);
if (d < 0) return g_manual; /* 时段未配置:回退 display.theme 手动深浅 */
return d ? MEOW_THEME_DARK : MEOW_THEME_LIGHT;
}
static void broadcast(void)
{
for (int i = 0; i < g_nlisteners; i++)
if (g_listeners[i]) g_listeners[i](g_mode);
}
const meow_theme *meow_theme_get(void)
{
if (!g_loaded) {
load_settings();
g_loaded = 1;
}
return resolve_mode(g_mode) == MEOW_THEME_DARK ? &g_dark : &g_light;
}
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 meow_theme_defaults(meow_theme_mode m)
{
switch (resolve_mode(m)) {
case MEOW_THEME_DARK: return g_dark;
case MEOW_THEME_LIGHT:
default: return g_light;
}
}
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;
}
}
}
+135
View File
@@ -0,0 +1,135 @@
/*
* 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 <stdlib.h>
#include <string.h>
enum { W_BTN = 0, W_SLIDER = 1, W_SWITCH = 2, W_LIST = 3 };
struct meow_widget {
int type;
int x, y, w, h;
char label[128];
int pressed; /* 按钮按下视觉态 */
int min, max, val; /* 滑条 */
int on; /* 开关 */
int count, sel; /* 列表 */
meow_list_source src;
void *ctx;
};
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;
if (label) { strncpy(t->label, label, sizeof t->label - 1); t->label[sizeof t->label - 1] = 0; }
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->min = min; t->max = (max > min) ? max : min + 1; t->val = (val < min) ? min : (val > t->max) ? t->max : val;
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->on = on ? 1 : 0;
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->count = count; t->src = src; t->ctx = ctx; t->sel = 0;
return t;
}
void meow_widget_free(meow_widget *w) { if (w) free(w); }
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) { return hit(w, x, y); }
int meow_btn_pressed(meow_widget *w) { return w && w->type == W_BTN ? w->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;
w->val = val;
}
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; }
int meow_switch_get(meow_widget *w) { return w && w->type == W_SWITCH ? w->on : 0; }
/* 供应用在 on_tap 里对外可控回馈:这里不存命中态(由 app 分发),仅暴露绘制 */
void meow_widget_draw(meow_widget *w)
{
if (!w) return;
const meow_theme *th = meow_theme_get();
switch (w->type) {
case W_BTN: {
uint32_t bg = w->pressed ? th->dim : th->accent;
meow_gfx_fill_round(w->x, w->y, w->w, w->h, 10, bg);
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, 0xFFFFFF);
break;
}
case W_SLIDER: {
int trace_y = w->y + w->h / 2 - 3;
meow_ui_fill_rect(w->x, trace_y, w->w, 6, th->dim);
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->accent);
int tx = w->x + frac;
int ty = w->y + w->h / 2;
meow_gfx_fill_round(tx - 10, ty - 10, 20, 20, 10, th->fg);
break;
}
case W_SWITCH: {
uint32_t bg = w->on ? th->accent : th->dim;
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);
break;
}
case W_LIST: {
int rowh = 44, pad = 16;
for (int i = 0; i < w->count; i++) {
int ry = w->y + i * rowh;
if (ry >= w->y + w->h) break;
if (i == w->sel) meow_ui_fill_rect(w->x, ry, w->w, rowh, th->accent);
char label[64] = "";
if (w->src && w->src(i, label, (int)sizeof label, w->ctx)) {
uint32_t col = (i == w->sel) ? 0xFFFFFF : th->fg;
meow_ui_text(w->x + pad, ry + (rowh - meow_ui_text_height(th->font_body)) / 2,
label, th->font_body, col);
}
meow_ui_fill_rect(w->x, ry + rowh - 1, w->w, 1, th->dim);
}
break;
}
default: break;
}
}
+13
View File
@@ -0,0 +1,13 @@
/*
* stb_image.c — stb_image 实现 TU。仅解 PNG/JPEG/BMP(其余格式关闭以减体积)。
* self-contained:PNG 内建 zlib 解码,不依赖系统 libz/libpng。
*/
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_PSD
#define STBI_NO_TGA
#define STBI_NO_GIF
#define STBI_NO_PIC
#define STBI_NO_PNM
#define STBI_NO_HDR
#define STBI_FAILURE_USERMSG
#include "stb_image.h"
+7988
View File
File diff suppressed because it is too large. Load diff