Files
AskaEth c0396e40ce sync(SDK): 增量同步主仓 M6.37–M6.39 至公开 SDK 仓
sdk/:
- 同步 meow_ui_widget/input/gfx/lazygrid/audio/media/img 等源码与头
  (SDK 默认列表/容器控件默认 MD3 — meow_list_set_container 可回退;
   全局 App 内底部上滑回桌面 — meow_ui_input_is_home_swipe 系统保留手势;
   LazyGrid/LazyColumn/LazyRow MD3 化;叠加让位自抑改 app_id 判定;
   DSD 音频格式等)
- 新增独立模块源码 meow_img_color / meow_lyric / meow_lyric_view 及对应宿主 test_*.c
- 保留公开仓自包含适配:vendored meow_font.c/meow_font.h/meow_notif.h 不动;
  build_sdk.sh 自包含版本不动;meow_ui_gfx.c 仅改 meow_font.h include 路径;
  meow_ui_internal.h 去除 FreeType 内部声明
- meow_ipc.h 同步到主仓 launcher 版本(含 AUDIO_FMT_DOP/媒体歌词/蓝牙输出协议)
- 不含 FreeType 矢量字 meow_ft_*(需 freetype vendor,公开仓以位图 meow_font
  自包含);不含 mipsel 二进制 / .o / .a / 固件 / 密钥 / devdocs

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

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

验证:sdk/meow_ui.a 与 examples/hello/hello 交叉编译均 0 warning;工作区无生成物。
2026-09-04 18:05:36 +08:00

1337 lines
63 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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_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/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 = <family> 或 "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) =================
* 复用 sb_icons.h(Material Symbols rounded,32x32 单通道 alpha)——状态栏右侧图标簇
* (meow_ui_sys_statusbar_render)与 TopAppBar 位图动作图标(W_TOPAPPBAR)共用同源位图。
* idx = sb_icons[] 索引(状态栏 SB_I_*、动作 MEOW_ICON_*,见 meow_ui_internal.h)
* x = 图标槽左缘(槽宽 SB_ICON_SIZE=32)
* ink_h = 目标 ink 高(>0 等比缩放;0 = 保持位图自然 ink 尺寸——WiFi 强弱档用)
* fg = 24-bit RGB 前景(主题 on_surface / on_surface_variant,日/夜由主题表提供)
* cy = ink 垂直中心线(状态栏=28px 栏约 14;TopAppBar=栏中点)
* 先求位图 ink 边界(阈值 32,忽略近透明抗锯齿),按 ink 在槽内水平居中、垂直中心对齐 cy;
* 像素按 alpha 软边混合进 back_buf(自 g_fb/g_line/g_xres/g_yres,与 launcher sb_blit_icon 同源算法)。 */
/* 像素核心:只采样混合进 back_buf,不标脏区。供状态栏路径(blit 后另标顶栏脏区)与
* 控件路径(meow_gfx_blit_icon_ink,控件 invalidation 独立管理脏区)共用,避免状态栏的
* 顶栏脏区假设污染控件绘制(topbar 不在屏幕最顶行时标错区域)。 */
static void icon_blit_px(int idx, int x, int ink_h, uint32_t fg, int cy)
{
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);
}
}
}
/* 状态栏路径:blit 像素 + 把整条顶栏标脏(present 只重画顶条即可)。 */
void meow_gfx_blit_icon(int idx, int x, int y, int ink_h, uint32_t fg, int cy)
{
(void)y;
if (!g_init_ok || !g_fb) return;
if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return;
icon_blit_px(idx, x, ink_h, fg, cy);
dirty_add(x, 0, SB_ICON_SIZE, MUI_SBAR_H);
}
/* 控件路径(TopAppBar 动作图标):只写像素,不标「顶栏」脏区——控件矩形由
* meow_widget_draw 的 clip + invalidation(setter 标脏)独立管理,避免把状态栏的
* 顶栏脏区假设带进控件绘制(topbar 不位于屏幕最顶行时标错区域、触发误重画)。 */
void meow_gfx_blit_icon_ink(int idx, int x, int ink_h, uint32_t fg, int cy)
{
if (!g_init_ok || !g_fb) return;
if (idx < 0 || idx >= (int)(sizeof sb_icons / sizeof sb_icons[0])) return;
icon_blit_px(idx, x, ink_h, fg, cy);
}
/* ================= 像素写入 ================= */
static void px_write_rgb565(int x, int y, uint16_t c16)
{
*(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);
}
/* ================= 矢量字(FreeType)渲染薄封装(§10.1) =================
* meow_ft_text 在 meow_ui_ft.c(独立 .o,App 显式调 meow_ft_* 才拉取整条 FreeType 依赖),
* 它经本处两个薄封装写进 back_buf:blend 一个灰度像素(过 clip)、标一块脏区。
* 这两函数在 gfx.o 里(任何 App 本就拉取 gfx.o),ft.o 引用它们不引入新的未拉入成员,
* 从而保证「不调 meow_ft_* 的 App 零增量」(freetype .o 不会被拉入)。 */
void meow_gfx_ft_blend_px(int x, int y, uint32_t color, int alpha)
{ put_px_blend(x, y, color, alpha); }
void meow_gfx_ft_dirty(int x, int y, int w, int h)
{ dirty_add(x, y, w, h); }
/* ================= RGBA blit(图片层用) ================= */
void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha)
{
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 / input 共用判定)----
* /tmp/meow_notif_active = 前台屏幕归属标记,**内容=当前前台 app_id**(空=菜单/叠加层覆盖/
* 无前台单元,即「抑制全部 App」)。App 进程(非系统 UI 渲染者)据此判「我是否前台」:
* - 内容==本进程 app_id(env MEOW_LOG_APP)→ 前台 → 照常上屏 / 收输入;
* - 内容为空 → 菜单前台或被叠加层覆盖 → 全抑制(含自身);
* - 内容为其它 app_id → 后台常驻(批①多 App)/非前台 → 自抑上屏 + 排空输入。
* 200ms 节流缓存存在性+内容(别每帧 open/stat)。
* 计数 g_overlay_suppressed 供宿主单测断言(meow_gfx_overlay_suppressed_count)。 */
static unsigned long long g_overlay_suppressed = 0;
/* §后台化保活-残帧:记录「上一次 present 是否处于叠加自抑态」,用于在抑制→不抑制的恢复边沿
* 强制整帧重画(盖掉 launcher 在后台化期间画进 buffer0 的桌面/主界面帧)。 */
static int g_was_overlay_suppressed = 0;
/* 本进程 app_id(env MEOW_LOG_APP,launcher 注入)= 判「我是否前台」的身份锚。 */
static const char *overlay_own_app(void)
{
static const char *s = NULL;
if (!s) {
const char *e = getenv("MEOW_LOG_APP");
s = (e && e[0]) ? e : "";
}
return s;
}
/* §批①多 App 后台常驻:present 层与 input 层共用的「是否自抑」判定(单一实现,防两端漂移)。
* 系统 UI(launcher 渲染面板/主界面)恒 0。返回 1=本进程应自抑上屏+排空输入。 */
int meow_ui_overlay_suppressed_active(void)
{
if (meow_ui_sys_render_enabled()) return 0; /* launcher(系统 UI)渲染面板,照常上屏 */
static char s_path[108];
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;
{
char owner[48];
int have = 0;
owner[0] = 0;
int fd = open(s_path, O_RDONLY);
if (fd >= 0) {
ssize_t n = read(fd, owner, sizeof(owner) - 1);
close(fd);
if (n > 0) owner[n] = 0;
have = 1; /* 文件存在(无论内容)→ 处于抑制管控 */
} else {
have = 0; /* 文件不存在 → 无抑制 */
}
if (!have) {
s_last_res = 0;
} else if (owner[0] == 0) {
s_last_res = 1; /* 空内容=全抑制(菜单前台/叠加层覆盖) */
} else {
const char *me = overlay_own_app();
/* 内容==本进程 app_id → 前台不抑制;否则(后台常驻/非前台)→ 抑制 */
s_last_res = (strcmp(owner, me) == 0) ? 0 : 1;
}
}
return s_last_res;
}
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 叠加层;后台常驻(批①)App 同判据自抑。
* 系统 UI 进程(launcher 渲染面板)不抑制。抑制期脏区保留累积(超上限自然升级整帧),
* 判定为「回到前台」后下一次 present 一次上屏。 */
if (meow_ui_overlay_suppressed_active()) { g_overlay_suppressed++; g_was_overlay_suppressed = 1; return; }
/* §后台化保活-残帧:抑制→不抑制的**恢复边沿**强制整帧重画。App 被 launcher 转后台保活
* (/tmp/meow_notif_active 存在)期间 present 被自抑,back_buf 仍保留 App 自己的完整帧;
* 但 launcher 已把桌面/主界面整帧画进 buffer0 盖在 App 残帧上。恢复前台时若只 present
* 抑制期累积的小脏区(时钟/状态栏),launcher 的桌面帧会在 App 未重画区域透出(真机
* 「桌面叠在 ncm UI 下面/旧播放界面层叠」。标记由有→无即「回到前台」,强制 dirty_full
* 让下一次 present 把 App 完整 back_buf 一次上屏,盖掉桌面帧。仅真实 App 进程(launcher
* 系统 UI 恒不抑制,本边沿不触发)。 */
if (g_was_overlay_suppressed) {
g_was_overlay_suppressed = 0;
dirty_full();
}
/* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现;
* 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */
unsigned long long now = now_ms();
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_<X>_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:<sig> 日志 */
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; }