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;工作区无生成物。
This commit is contained in:
AskaEth committed 2026-09-04 18:05:36 +08:00
1 parent a1687486bc
commit c0396e40ce
45 files changed
+4665 -175

No files matched your search

+89 -8
View File
@@ -81,6 +81,7 @@ enum {
MSG_PICKER_RESULT = 0x0071, /* ↑ picker 返回值(候选结果回 A;→launcher→amsd) */
MSG_META = 0x0072, /* ↑ 媒体会话上报元数据/进度/状态(阶段 D 附录 B) */
MSG_PLAYCTL = 0x0073, /* ↓ 播放控制下发/↑ 控制器转发到最近活跃媒体会话 */
MSG_META_LYRIC = 0x0074, /* ↑ 媒体会话上报当前曲歌词 lrc/tlrc(阶段 D · 批3,分片) */
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
@@ -99,6 +100,10 @@ enum {
MSG_AUDIO_STATE = 0x00C8, /* ↓ 状态快照文本(NUL 结尾;当前流/参数/帧数/underrun/路由/音量) */
MSG_AUDIO_ROUTE = 0x00C9, /* ↑ 手动路由(u32 port:0=auto 2=3.5 3=4.4;调试/面板用) */
MSG_AUDIO_VOLUME = 0x00CA, /* ↑ 音量(i32 pct:-1=查询 0-100=设置;v1 透传写 DAC numid=1/2) */
/* P2 蓝牙(BLUETOOTH_FULL_OVERHAUL_PLAN §五):输出目标切换。audiod 读 settings.conf
* `audio.output_bt`(0/1) 或收本消息改写目标(0=OUT_DAC 1=OUT_BT),重配 PCM。
* 与物理路由 numid=9 解耦:蓝牙非物理端口,不写 numid=9、不触发 FPGA 烧录。 */
MSG_AUDIO_OUTPUT = 0x00CB, /* ↑ 输出目标(u32 target:0=DAC 1=BT;ui/CLI 下发) */
/* ---- meow_powermgr 功耗管理服务(devdocs/plan/POWER_MANAGER_DESIGN.md §6.1):0x00D0-0x00D7 ----
* 语义:功耗维度的查看/分析/管理(svc_power 管动作、svc_perf 管性能,powermgr 管功耗)。
* 独立 socket /usr/data/meow_pm.sock(--sock/MEOW_PM_SOCK 覆盖)。鉴权分级同 svc_power:
@@ -274,6 +279,29 @@ typedef struct m_playctl {
int64_t seek_ms; /* cmd=5: 目标位置 ms;否则 0 */
} m_playctl_t; /* 4 (+4 pad) + 8 = 16 B(mipsel int64 8 对齐) */
/* ---- 媒体会话歌词上报(阶段 D · 批3:live lyric 数据源框架)----
* App 播放时把当前曲歌词(lrc 原文 + tlrc 翻译)上报到 meow_media 会话,供桌面歌词页/播放页
* 随播放位置实时滚动。约束:launcher 环缓冲 `payload[MEOW_IPC_CARGO(1024)]` 为单帧入队上限,
* 故歌词按分片传输(每片 ≤ MEOW_LYRIC_CHUNK 字节)。
* - seq = 歌曲代际(换歌/换词自增;launcher 见新 seq 清旧词缓存,同曲分片共用)
* - kind = 0 原文 lrc / 1 翻译 tlrc
* - total= 该 kind 总字节数(不含 NUL;0=清空该 kind)
* - off/len = 本片起始偏移 / 有效字节(首片 off=0;分片按序发送)
* 边界:lrc 上限 MEOW_MEDIA_LRC_MAX / tlrc 上限 MEOW_MEDIA_TLRC_MAX(会话内每 App 一份缓冲,
* 超限截断)。payload 定长且 == MEOW_IPC_CARGO(即 sizeof(m_lyric_t)=1024)。 */
#define MEOW_MEDIA_LRC_MAX 3584u /* 会话内歌词原文缓冲上限 */
#define MEOW_MEDIA_TLRC_MAX 1568u /* 会话内歌词翻译缓冲上限 */
#define MEOW_LYRIC_CHUNK (MEOW_IPC_CARGO - 44u) /* 单分片数据上限(44=头:32+4+2+2+2+2) */
typedef struct m_lyric {
char app_id[MEOW_APP_ID_MAX]; /* 32: 上报源头(与 HELLO/META 的 app_id 一致) */
uint32_t seq; /* 歌曲代际(新歌/新词自增;分片同 seq) */
uint16_t kind; /* 0=原文 lrc;1=翻译 tlrc */
uint16_t total; /* 该 kind 总字节数(不含 NUL;0=清空该 kind) */
uint16_t off; /* 本片起始偏移(≤ total;首片=0) */
uint16_t len; /* 本片有效字节(≤ MEOW_LYRIC_CHUNK) */
char data[MEOW_LYRIC_CHUNK]; /* 本片内容(len 后 NUL 结尾,接收侧再截断) */
} m_lyric_t; /* 44 + 980 = 1024 B = MEOW_IPC_CARGO */
/* MSG_NOTIFY ↑:推送/更新通知(freedesktop 对齐 + MeowOS 扩展;定长 LE)。
* - summary/body/app_icon/expire_timeout 直通 freedesktop 字段;app_icon 即 SDK `icon`
* (icon name 或 path;映射为空串时 launcher 用该 app 默认图标回退);
@@ -457,24 +485,32 @@ typedef struct m_picker_result {
/* ================= audiod 独占音频服务(AUDIOD_DESIGN.md) = */
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE。
* v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32 处理;FLOAT v1 拒绝。 */
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE
* 5=AUDIO_FMT_DOP(DoP DSD 透明容器格式)。v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32
* 处理;FLOAT v1 拒绝。AUDIO_FMT_DOP 为 **独立格式值**(A3+DSD 定案:避免 S24 名义(3B)/实际(4B)歧义),
* 仅在 MSG_AUDIO_OPEN.flags 带 AUDIO_FLAG_DSD 时合法;audiod 只做 DoP 容器透传(不引 dsd2pcm)。 */
enum {
AUDIO_FMT_S16 = 1,
AUDIO_FMT_S24 = 2,
AUDIO_FMT_S32 = 3,
AUDIO_FMT_FLOAT = 4
AUDIO_FMT_FLOAT = 4,
AUDIO_FMT_DOP = 5 /* DoP DSD(32-bit 容器:bits[31:16]=16bit DSD 数据,bits[15:8]=标记 0xFA/0x05,bits[7:0]=0),
容器率 = DSD 位率/16(DSD64→176400 / DSD128→352800 / DSD256→705600) */
};
/* MSG_AUDIO_OPEN.flags 位(预留曾恒 0;A3+DSD 起使用) */
#define AUDIO_FLAG_DSD 0x00000001u /* fmt==AUDIO_FMT_DOP:本流为 DoP DSD(App 侧已解码),audiod 只透传 */
/* MSG_AUDIO_OPEN ↑:请求开流(App 原生声源参数)。grant_*(ACK)回 audiod 实际授权给客户端的
* 产出格式(率/声道/位深)——客户端必须按 grant 格式产出 PCM(SDK meow_ui_audio 负责归一:
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。 */
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。fmt==AUDIO_FMT_DOP
* 须带 AUDIO_FLAG_DSD(否则 ERR_INVAL);DSD 经 DoP 透传,audiod 不引 dsd2pcm。 */
typedef struct m_audio_open {
char app_id[MEOW_APP_ID_MAX]; /* 32 */
uint32_t rate; /* 原生采样率(应用源,如 44100/48000) */
uint32_t rate; /* 原生采样率(应用源,如 44100/48000;DSD 容器率) */
uint16_t ch; /* 原生声道数(1=mono 2=stereo) */
uint16_t fmt; /* 原生格式(AUDIO_FMT_*) */
uint32_t flags; /* 预留(0) */
uint16_t fmt; /* 原生格式(AUDIO_FMT_*;DSD 用 AUDIO_FMT_DOP) */
uint32_t flags; /* AUDIO_FLAG_DSD 等 */
} m_audio_open_t; /* 32+4+2+2+4 = 44 B */
/* MSG_AUDIO_OPEN_ACK ↓:授权结果。err=MEOW_OK 才有效(stream_id>0 且 grant_* 为实际授权值);
@@ -505,11 +541,24 @@ typedef struct m_audio_route {
} m_audio_route_t; /* 4 B */
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
* + 持久化 settings.conf `audio.volume`,与 watchdog/launcher 同源同键)。 */
* + **DSD_VOLUME(音量收编 v2 泛化双组写,保证 DSD↔PCM 切换连续/防爆音)** + 持久化 settings.conf
* `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时**不下发 DAC 音量反转**
* (音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
typedef struct m_audio_volume {
int32_t pct; /* -1=查询;0-100=设置 */
} m_audio_volume_t; /* 4 B */
/* 输出目标枚举(msg AUDIO_OUTPUT / settings.conf `audio.output_bt` 共用;亦用于 CLI output) */
enum {
AUDIO_OUT_DAC = 0, /* 本地 DAC(hw:0,0,独占/period=1024×4/FPGA numid=9/音量反转) */
AUDIO_OUT_BT = 1 /* 蓝牙 A2DP source(pcm.bluealsa 播放向,透传+格式归一,AVRCP 控音量) */
};
/* MSG_AUDIO_OUTPUT ↑:切换输出目标(ui/CLI 下发)。0=DAC 1=BT。audiod 侧重配 PCM。 */
typedef struct m_audio_output {
uint32_t target; /* AUDIO_OUT_DAC / AUDIO_OUT_BT */
} m_audio_output_t; /* 4 B */
/* ================= meow_netmgrd 网络管理服务(devdocs/plan/NETMGR_DESIGN.md §3/§10)=====
* 语义:接口统一层(WiFi/BT/USB-eth/rfkill/IP/路由/DNS 只读)查询与控制的系统权威。
* 独立 socket /usr/data/meow_net.sock(--sock/MEOW_NET_SOCK 覆盖)。进程形态对齐 powermgr:
@@ -607,6 +656,38 @@ typedef struct m_net_dns_result {
char text[MEOW_IPC_MAX - 8];
} m_net_dns_result_t;
/* ---- 期 3:管控层(NETMGR_DESIGN §6;默认 allow,按实体手动 block + 域名白/黑)----
* 0x00EA MSG_NET_POLICY(同时承载 GET/SET,op 区分)。GET=读级任意;SET=写级仅系统身份
* (SO_PEERCRED → /usr/bin/meow_* / os.meow.* / MeowPanel)。防自锁实体(dropbear/wpa_supplicant/
* udhcpc/meow_netmgrd/meow_svcd/meow_sysui/init)拒绝置 block(服务端硬校验)。
* 策略持久化:`/data/meow/netmgrd/net_policy.conf`(/data=/usr/data 符号链接),读改写单键原子写。 */
enum { /* 与主 enum 分列,追加式不改既有行 */
MSG_NET_POLICY = 0x00EA, /* ↑ op=0 查询 / op=1 写(set net / set domain / del) */
MSG_NET_POLICY_RESULT = 0x00EB /* ↓ 策略文本/结果 */
/* 0x00EE 事件推送(预留,期2 订阅制)/ 0xEF 余量(均为预留) */
};
/* MSG_NET_POLICY 操作码 */
enum {
NET_POLICY_SHOW = 0, /* show [entity]:entity 空=全部 */
NET_POLICY_SET_NET = 1, /* set <entity> allow|block */
NET_POLICY_SET_DOMAIN = 2, /* domain <entity> +host|-host */
NET_POLICY_DEL = 3 /* del <entity> */
};
typedef struct m_net_policy {
uint32_t op; /* NET_POLICY_* */
char entity[48]; /* app:<pkg> | svc:<svc> | proc:<comm> | global;空=适用 SHOW 的全量 */
char arg[64]; /* SET_NET:allow|block;SET_DOMAIN:+host|-host;其余空 */
} m_net_policy_t; /* 120 B */
typedef struct m_net_policy_result {
uint32_t code; /* MEOW_OK / MEOW_ERR_PERM / MEOW_ERR_INVAL / (防自锁)MEOW_ERR_PERM */
uint16_t text_len;
uint16_t pad;
char text[MEOW_IPC_MAX - 8];
} m_net_policy_result_t;
/* ---- 期 4:SDK 网络自愿通道(NETMGR_DESIGN §6.3/§4;0xEE/0xEF)----
* 一、meow_net_policy_check(host):框架 App 联网前自查 host 是否被 netmgrd 策略放行——
* 服务端以调用方 app_id(msg.app_id)→ 实体 app:<pkg>,评估 policy_net_blocked(entity)
+9
View File
@@ -38,6 +38,7 @@ typedef struct meow_audio_state {
int paused; /* 当前流是否暂停 */
int route_port; /* 当前路由口(-1 未知) */
int vol; /* 当前音量 0-100 */
int dsd; /* 当前流是否为 DoP DSD(fmt==AUDIO_FMT_DOP 且 audiod 已授权) */
} meow_audio_state;
/* 被抢占回调:by_app=抢占者 app_id(非空)。ctx 由 on_preempt 注册时传入。 */
@@ -48,6 +49,10 @@ typedef void (*meow_audio_preempt_fn)(const char *by_app, void *ctx);
* MEOW_ERR_NODEV=无可用 PCM 设备;MEOW_ERR_* 常量本身即负值)。open 后客户端须按内部记录的
* grant 格式产出 PCM。 */
int meow_audio_open(int rate, int ch, int fmt);
/* 开流(带 flags)。flags=AUDIO_FLAG_DSD 且 fmt=AUDIO_FMT_DOP 时为 DoP DSD 流(A3+DSD 定案):
* App 侧须把 DSD 解码封装为 DoP(每样本 32-bit 容器:16bit DSD 数据 + 标记 + 0),audiod 只透传
* (不引 dsd2pcm、不做 DOP_EN 之外的任何处理)。DSD 打不开/不支持 → open 返回负错误码(报错关流)。 */
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags);
/* 写 PCM。pcm=native fmt/ch 样本(open 传的参数),frames=帧数;内部归一成 grant 并分批发送。
* 返回写入帧数;<0=流已死(被抢占/audiod 断开),App 应重新 open。阻塞(socket 背压)。 */
int meow_audio_write(const void *pcm, int frames);
@@ -57,6 +62,10 @@ int meow_audio_pause(int sid, int paused);
int meow_audio_close(int sid);
/* 查询 audiod 快照(填充 out)。返回 0 成功;<0 失败。 */
int meow_audio_query(meow_audio_state *out);
/* 设置音量 0-100(audiod 唯一权威:写 DAC numid=1/2 + DSD_VOLUME 双组保切换连续 + 持久化
* settings.conf audio.volume,与 launcher/watchdog 同源同键)。BT 输出目标时音量由 AVRCP 侧控,
* audiod 不下发 DAC 反转。返回 0 成功;<0 失败(audiod 不在)。 */
int meow_audio_set_volume(int pct);
/* 注册被抢占回调(可选;内部读线程分发)。重复调用幂等。 */
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx);
+5 -2
View File
@@ -22,8 +22,11 @@ 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);
int meow_img_w(const meow_img *img);
int meow_img_h(const meow_img *img);
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque,
* 本函数为其唯一像素读入口;NULL 返回 NULL。 */
const unsigned char *meow_img_pixels(const meow_img *img);
void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */
/* 内存瘦身:最近邻等比缩放到最长边 ≤maxdim,返回新图;原图已 ≤maxdim 返回 NULL。
+7
View File
@@ -36,6 +36,13 @@ void meow_ui_request_exit(void); /* 请求退出(相当于返回/
* x<=0 时清掉此前画的条(填回背景色)。不发 flush,由调用方 meow_ui_flush_region 上屏。 */
void meow_ui_swipe_hint(int x);
/* 批②(2026-09-04):底部热区上滑 → 回桌面(home)判定,镜像 launcher np_gesture.h
* np_home_gesture 同口径。down_x/down_y=抬手原点,dx/dy=抬手位移(释放-原点),
* fb_h=屏高(SDK 传 meow_ui_height())。命中=起点在底部热区(最后 HOME_EDGE_H≈28px)且
* **不在左缘**(down_x>=EDGE_W,与左缘返回互斥)+ 上移 ≥HOME_SWIPE(65px) + 竖向主导(adx<-dy)。
* 供宿主单测直调;触摸状态机(meow_ui_input.c touch_release)与 App 内判据共用,避免两端漂移。 */
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h);
/* ============================================================ */
/* §6.3 通用长按(2026-08-31) */
/* ============================================================ */
+21
View File
@@ -49,6 +49,7 @@ int meow_btn_pressed(meow_widget *w); /* 应用在 on_tap 命中后
/* ---- §5.8.2 Modifier 式通用 Options(轻量结构体,非链式 DSL)----
* 经 meow_widget_set_opts 一次性挂载,作用于任意控件(标准/自定义)。全部控件共用。
* 哨兵语义:bg/fg==0=用控件/主题默认;stroke==0=无描边;stroke_w=描边宽;
* 控件语义:W_PROGRESS 用 bg=轨道(0=surfaceVariant)、fg=填充(0=primary);见 meow_ui_widget.c 批5b;
* radius==-1=控件默认(如按钮 10),>=0=指定圆角(0=直角);pad/margin=0=无;
* disabled/selected=1=置位(立即反映到状态机位)。 */
typedef struct meow_widget_opts {
@@ -145,6 +146,17 @@ enum {
meow_widget *meow_btn_create_variant(int x, int y, int w, int h, const char *label, int variant);
int meow_btn_set_variant(meow_widget *w, int variant); /* 越界 clamp;0=成功 */
/* ---- MD3 shape 层级(§5.8.6,Material You 圆角体系,单位 px)----
* 组件按角色落到五档 + 胶囊:按钮=full(全高一半)、对话框=extra-large(28)、卡=large(16)、
* 输入框=small(8)、勾选=extrasmall(4)。theme.radius/radius_card 为风格族默认;App 可经
* opts.radius 逐控件覆盖。全高一半用 MEOW_SHAPE_FULL(h) 宏(控件的胶囊圆角)。 */
#define MEOW_SHAPE_EXTRASMALL 4 /* 勾选 Checkbox/Radio */
#define MEOW_SHAPE_SMALL 8 /* 输入框 TextField */
#define MEOW_SHAPE_MEDIUM 12 /* 主题默认 radius(列表卡/通用控件) */
#define MEOW_SHAPE_LARGE 16 /* 卡片/容器(theme.radius_card 默认) */
#define MEOW_SHAPE_EXTRA_LARGE 28 /* 对话框 Dialog(MD3 标准 extra-large) */
#define MEOW_SHAPE_FULL(h) ((h) / 2) /* 全形=胶囊(按钮/导航等) */
/* ---- TextField(单行文本输入,接 IME §5.1)---- */
/* placeholder=提示文案;buf=App 持有的文本缓冲(组件不回拷,只绘制/编辑其上);maxlen=含 NUL 预算 */
meow_widget *meow_textfield_create(int x, int y, int w, int h, const char *placeholder,
@@ -254,6 +266,10 @@ int meow_list_selected(meow_widget *w);
typedef void (*meow_list_tap_fn)(int idx, void *ctx);
void meow_list_set_on_tap(meow_widget *w, meow_list_tap_fn cb, void *ctx); /* 命中行→回调 */
int meow_list_tap_at(meow_widget *w, int x, int y); /* 命中行:置选中+回调,返回 idx/-1 */
/* MD3 容器模式(W_LIST 共用,§5.5/§5.8.6):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
int meow_list_set_container(meow_widget *w, int on);
/* ============================================================ */
/* §5.5 设计模板 A② 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作 + 选中
@@ -383,6 +399,11 @@ int meow_listview_set_item_tier(meow_widget *lv, int lines);
/* 完全自绘:设 draw_item 回调(覆盖默认 renderer);NULL=用默认 renderer。返回 0=成功。 */
int meow_listview_set_item_draw(meow_widget *lv, meow_listview_draw_fn draw,
void *ctx);
/* MD3 容器模式(默认 renderer 可选,P0 对齐):0=扁平满宽行+分隔线(默认,兼容);
* 1=每行外套 surfaceContainer 圆角卡(12dp)+行间空隙,选中=primaryContainer/按压=primary α
* (tonal,非满幅实心),去冗余分隔线;段头=surface 上的 primary 段标签(无卡无分隔线)。
* 注意:仅对默认 renderer(未 set_item_draw)生效;完全自绘行由 App self-draw 自理。 */
int meow_listview_set_container(meow_widget *lv, int on);
/* 空态占位:count==0 时视口内居中画 empty_text(NULL 不画)。返回 0=成功。 */
int meow_listview_set_empty_text(meow_widget *lv, const char *text);
+151
View File
@@ -0,0 +1,151 @@
/*
* meow_img_color.c — MeowOS SDK 封面双色渐变取色实现(见 meow_img_color.h 契约)。
*
* 独立源码,不并入 meow_ui.a(铁律:避免并发重编/四 App 重链)。取色算法纯函数(无 GPU/浮点/堆)。
* 唯一 meow_* 依赖:仅 meow_img_pixels(SDK 正式访问器,meow_ui_img.c §4),并经 meow_img_w/h 取宽高
* ——三者读 meow_img 的唯一入口,本 TU 不再局部补全其结构体(agent-402 遗留点已上收)。App/宿主
* 链入 meow_img_color.c 时需能解析 meow_img_pixels/w/h(宿主测试已链 meow_ui_img.c,实际 App 恒链
* meow_ui.a)。
*/
#include "meow_img_color.h"
#include <string.h>
#define MIC_BUCKETS 4 /* 每通道色阶数 ⇒ 4×4×4=64 桶 */
#define MIC_NBUCKET (MIC_BUCKETS*MIC_BUCKETS*MIC_BUCKETS)
#define MIC_ALPHA_MIN 64 /* 跳过近透明(alpha<64) */
#define MIC_LUMA_MIN 24 /* 跳过近黑 */
#define MIC_LUMA_MAX 244 /* 跳过近白 */
#define MIC_MIN_VALID 8 /* 有效采样下限(低于=取色失败回退主题) */
#define MIC_TARGET 900 /* 目标采样数(副采样步进自适,×4 作上限) */
#define MIC_SEC_FRAC 7U /* 次桶显著门槛:≥主桶 1/7(≈14%) */
static int mic_luma(uint32_t c)
{
int r = (int)((c >> 16) & 255), g = (int)((c >> 8) & 255), b = (int)(c & 255);
return (r * 299 + g * 587 + b * 114) / 1000;
}
/* 两色线性混合(t=0..255,0=纯 a、255=纯 b;逐通道整数,无浮点)。 */
static uint32_t mic_mix(uint32_t a, uint32_t b, int t)
{
if (t < 0) t = 0;
if (t > 255) t = 255;
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
int r = ar + ((br - ar) * t + 128) / 255;
int g = ag + ((bg - ag) * t + 128) / 255;
int bl = ab + ((bb - ab) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
}
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
uint32_t *main, uint32_t *second)
{
if (!rgba || w <= 0 || h <= 0 || !main || !second) return -1;
/* 副采样步进:使采样数≈MIC_TARGET(×4 上限),大图/缩略图成本恒定。 */
int step = 1;
while ((long)(w / step) * (long)(h / step) > (long)MIC_TARGET * 4 && step < 64)
step++;
long cnt[MIC_NBUCKET];
long sr[MIC_NBUCKET], sg[MIC_NBUCKET], sb[MIC_NBUCKET], sw[MIC_NBUCKET];
memset(cnt, 0, sizeof cnt);
memset(sr, 0, sizeof sr);
memset(sg, 0, sizeof sg);
memset(sb, 0, sizeof sb);
memset(sw, 0, sizeof sw);
long total = 0;
for (int y = 0; y < h; y += step) {
for (int x = 0; x < w; x += step) {
const uint8_t *p = rgba + ((size_t)y * (size_t)w + (size_t)x) * 4;
int a = p[3];
if (a < MIC_ALPHA_MIN) continue;
int r = p[0], g = p[1], b = p[2];
if ((r * 299 + g * 587 + b * 114) / 1000 < MIC_LUMA_MIN) continue; /* 近黑 */
if ((r * 299 + g * 587 + b * 114) / 1000 > MIC_LUMA_MAX) continue; /* 近白 */
int mx = r, mn = r;
if (g > mx) mx = g;
if (b > mx) mx = b;
if (g < mn) mn = g;
if (b < mn) mn = b;
int sat = mx - mn; /* 饱和度 0..255 */
long wt = 1 + sat / 24; /* 1..~11:偏向鲜艳(跳过灰蒙蒙) */
int qr = r * MIC_BUCKETS / 256, qg = g * MIC_BUCKETS / 256, qb = b * MIC_BUCKETS / 256;
int bi = (qr * MIC_BUCKETS + qg) * MIC_BUCKETS + qb;
if (bi < 0 || bi >= MIC_NBUCKET) continue;
cnt[bi] += wt;
sr[bi] += (long)r * wt; sg[bi] += (long)g * wt; sb[bi] += (long)b * wt;
sw[bi] += wt;
total++;
}
}
if (total < MIC_MIN_VALID) return -1; /* 全透明/近黑白 → 失败回退主题 */
/* 主色:加权最大桶中心 */
int mi = -1;
long best = -1;
for (int i = 0; i < MIC_NBUCKET; i++)
if (cnt[i] > best) { best = cnt[i]; mi = i; }
if (mi < 0 || sw[mi] <= 0) return -1;
long mr0 = sr[mi] / sw[mi], mg0 = sg[mi] / sw[mi], mb0 = sb[mi] / sw[mi];
*main = ((uint32_t)mr0 << 16) | ((uint32_t)mg0 << 8) | (uint32_t)mb0;
/* 次色:与主色 RGB 色距最大的显著桶;无显著桶→主色缩暗 2/3(渐变互补)。 */
long thresh = best > 0 ? best / MIC_SEC_FRAC : 1;
int si = -1;
long sd = -1;
for (int i = 0; i < MIC_NBUCKET; i++) {
if (i == mi || cnt[i] < thresh || sw[i] <= 0) continue;
long dr = sr[i] / sw[i] - mr0, dg = sg[i] / sw[i] - mg0, db = sb[i] / sw[i] - mb0;
long d = dr * dr + dg * dg + db * db;
if (d > sd) { sd = d; si = i; }
}
if (si >= 0)
*second = ((uint32_t)(sr[si] / sw[si]) << 16) | ((uint32_t)(sg[si] / sw[si]) << 8) |
(uint32_t)(sb[si] / sw[si]);
else
*second = ((uint32_t)(mr0 * 2 / 3) << 16) | ((uint32_t)(mg0 * 2 / 3) << 8) |
(uint32_t)(mb0 * 2 / 3);
return 0;
}
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second)
{
if (!im) return -1;
/* 经 SDK 正式访问器读像素(meow_img opaque;meow_img_pixels 为唯一读入口)。 */
const unsigned char *rgba = meow_img_pixels(im);
if (!rgba) return -1;
return meow_img_duocolor_rgba(rgba, meow_img_w(im), meow_img_h(im), main, second);
}
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
meow_img_color_theme *out)
{
if (!out) return;
uint32_t mainv = main ? main : (th ? th->primary : 0x808080);
uint32_t secv = second ? second : (th ? th->secondary : mainv);
/* 背景平均亮度 → 文字可读色(黑/白) */
int luavg = (mic_luma(mainv) + mic_luma(secv)) / 2;
uint32_t fg = (luavg >= 140) ? 0x1C1C1C : 0xFFFFFF;
uint32_t fgd = mic_mix(fg, mic_mix(mainv, 0x0A0A0A, 150), 90); /* 次要文字向背景靠(降对比) */
/* 背景渐变:主/次各压暗 55%(防刺眼 + 保文字可读) */
uint32_t bg_top = mic_mix(mainv, 0x0A0A0A, 150);
uint32_t bg_bottom = mic_mix(secv, 0x0A0A0A, 150);
/* 强调=次色(与主色底对比);其上图标按 accent 亮度选黑/白 */
uint32_t accent = secv;
uint32_t accent_fg = (mic_luma(accent) >= 140) ? 0x1C1C1C : 0xFFFFFF;
out->bg_top = bg_top; out->bg_bottom = bg_bottom;
out->fg = fg; out->fg_dim = fgd;
out->accent = accent; out->accent_fg = accent_fg;
/* 歌词页覆盖:scrim=主色压暗、cur=fg(可读)、gray 留 0(lyric_view 以 cur 自动递减)。 */
meow_lyric_view_colors *lc = &out->lyr;
memset(lc, 0, sizeof *lc);
lc->scrim = mic_mix(mainv, 0x000000, 150);
lc->cur = fg;
lc->scrim_alpha = 176;
}
+67
View File
@@ -0,0 +1,67 @@
/*
* meow_img_color.h — MeowOS SDK 封面双色渐变取色(批5)。
*
* 定位:从歌曲封面(已解码 RGBA 像素/meow_img 句柄)提取「主色 + 次色」,供播放页/歌词页动态主题
* (背景渐变/文字 accent)。纯 C 分桶统计,无 GPU、无浮点、无堆分配。
*
* 组织:独立公共头(仿 meow_lyric_view.h 模式,不随 meow_ui.h 伞头发布),取色原语是独立源码
* meow_img_color.c——**不并入 meow_ui.a**(铁律:避免并发重编/四 App 重链)。App/launcher 需显式
* `#include "meow_img_color.h"` 并在构建时单链 meow_img_color.c(宿主单测同法)。
*
* 约束(0 warning / 最小侵入):
* - meow_img 对外保持 opaque;像素直读经 SDK 正式访问器 `meow_img_pixels`(meow_ui_img.c §4,
* 已上收「局部补全结构体」遗留),宽高经 `meow_img_w`/`meow_img_h`。本 TU 不再局部补全布局。
* - 取色算法纯函数:meow_img_duocolor_rgba / meow_img_color_derive 纯 C、无 meow_* 依赖;
* meow_img_duocolor(句柄版)仅经上述三访问器读,不触碰 meow_ui.a 内部实现。宿主单测链
* meow_img_color.c + meow_ui_img.c 即可。
*
* 色彩语义:
* - 输入 RGBA(stb_image 输出,4 通道 straight alpha),w×h,最多遍历 ~3.6k 采样(内部自适
* 副采样步进,等效缩略图成本,避免全尺寸逐像素)。
* - 色域分桶 4×4×4=64(RGB 各 4 段),跳过近黑(luma<24)/近白(luma>244)/透明(alpha<64),
* 按「像素数 + 饱和度」加权;主色=加权最大桶中心;次色=与主色色距最大的显著桶(≥主桶 14%)
* 中心;无显著桶→次色=主色缩暗 2/3(渐变互补)。
* - 返回 0=成功;-1=参数非法 / 有效采样不足(调用方回退全局 meow_theme)。
*/
#ifndef MEOW_IMG_COLOR_H
#define MEOW_IMG_COLOR_H
#include "meow_ui.h" /* meow_img 句柄 + meow_theme */
#include "meow_lyric_view.h" /* meow_lyric_view_colors(派生歌词页覆盖) */
#ifdef __cplusplus
extern "C" {
#endif
/* 从 RGBA 像素缓冲取主色+次色(纯算法;样本副采样)。返回 0=成功;-1=失败。 */
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
uint32_t *main, uint32_t *second);
/* 从 meow_img 句柄取主色+次色(经 meow_img_pixels 读像素,见 .c)。返回 0=成功;-1=失败。 */
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second);
/* 由主/次色派生播放页 + 歌词页封面主题(纯函数,可宿主单测)。
* th 用于默认回退色(main/second 为 0 时);调用方应传 meow_theme_get()。
* 派生语义:
* - 背景渐变 = 主色(顶)→ 次色(底)各压暗 55%(防刺眼 + 保文字可读);
* - fg = 可读文字色(按主次平均亮度选黑/白),fg_dim = 向背景靠拢的次要文字色;
* - accent = 次色(与主色底对比),accent_fg = accent 上可读图标色;
* - lyr = 歌词页 set_colors 覆盖:scrim=主色压暗(alpha 176)、cur=fg,gray 留 0(由 lyric_view
* 以「cur 为基准自动递减」——批5 已修正对覆盖 cur 亦然)。 */
typedef struct meow_img_color_theme {
uint32_t bg_top; /* 播放页渐变顶 */
uint32_t bg_bottom; /* 播放页渐变底 */
uint32_t fg; /* 主要文字色(背景上可读) */
uint32_t fg_dim; /* 次要文字色 */
uint32_t accent; /* 强调色(控件/进度/封面占位) */
uint32_t accent_fg; /* accent 上可读前景(图标) */
meow_lyric_view_colors lyr; /* 歌词页 set_colors 覆盖 */
} meow_img_color_theme;
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
meow_img_color_theme *out);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_IMG_COLOR_H */
+24 -13
View File
@@ -268,35 +268,46 @@ int meow_lazygrid_hit(meow_lazygrid *g, int x, int y)
}
/* ---- 绘制(只画可见行;clip 钳到宫格视口)---- */
/* MD3 容器(默认 renderer P0):格=格内对称内缩 surfaceContainer 圆角卡(radius_card)+ 卡内 tonal
* 选中(primaryContainer)/ 按压(primary α26);画布=整格(触控目标),卡在其内对称内缩(同
* meow_listview 行>卡范式:卡片外仍命中整格,视觉卡留出呼吸空隙)。完全自绘(set_item_draw)
* 覆盖本 renderer,App 自理容器观感。 */
#define MEOW_LAZYGRID_CARD_MARGIN 8 /* 格内对称边距(卡内缩;与 cell gap 叠加出卡间空隙) */
static void lg_draw_cell(meow_lazygrid *g, const meow_theme *th, int index,
int cell_x, int cell_y, meow_lazygrid_item *it)
{
int sel = (index == g->sel);
int txtcol = sel ? th->onPrimaryContainer : th->onSurface;
int subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
uint32_t bg = sel ? th->primaryContainer : th->surfaceContainerLow;
meow_gfx_fill_round(cell_x, cell_y, g->cell_w, g->cell_h, th->radius_card, bg);
int m = MEOW_LAZYGRID_CARD_MARGIN;
int cw = g->cell_w - 2 * m, chh = g->cell_h - 2 * m; /* 卡尺寸(格内对称内缩) */
int cx = cell_x + m, cy = cell_y + m; /* 卡左上 */
int press = meow_input_touch_down() &&
meow_input_touch_x() >= cell_x && meow_input_touch_x() < cell_x + g->cell_w &&
meow_input_touch_y() >= cell_y && meow_input_touch_y() < cell_y + g->cell_h;
meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->surfaceContainer);
if (sel) meow_gfx_fill_round(cx, cy, cw, chh, th->radius_card, th->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(cx, cy, cw, chh, th->radius_card, th->primary, 26);
int pad = 8;
int thh = meow_ui_text_height(th->font_body);
int chh = g->cell_h, cw = g->cell_w;
/* 图标(可选):居中于格顶 ~50%。 */
/* 图标(可选):居中于卡顶 ~50%(卡中心=格中心,因对称内缩)。 */
if (it->icon) {
int isz = cw * 6 / 10; if (isz > chh * 5 / 10) isz = chh * 5 / 10;
int isz = g->cell_w * 6 / 10; if (isz > g->cell_h * 5 / 10) isz = g->cell_h * 5 / 10;
if (isz < 16) isz = 16;
int icx = cell_x + cw / 2, icy = cell_y + pad + isz / 2;
int icx = cx + cw / 2, icy = cy + pad + isz / 2;
meow_ui_img_scaled(icx - isz / 2, icy - isz / 2, isz, isz, (meow_img *)it->icon);
}
/* 标题 + 副标题(主界面:名 + 状态)。底部对齐。 */
/* 标题 + 副标题(主界面:名 + 状态)。卡内底部对齐。 */
int has_sub = (it->subtitle && it->subtitle[0]);
int ty = cell_y + chh - pad - thh - (has_sub ? (thh + 2) : 0);
if (it->title) meow_ui_text_center(cell_x + cw / 2, ty, it->title, th->font_body, txtcol);
if (has_sub) meow_ui_text_center(cell_x + cw / 2, ty + thh + 2, it->subtitle, th->font_body, subcol);
int ty = cy + chh - pad - thh - (has_sub ? (thh + 2) : 0);
if (it->title) meow_ui_text_center(cx + cw / 2, ty, it->title, th->font_body, txtcol);
if (has_sub) meow_ui_text_center(cx + cw / 2, ty + thh + 2, it->subtitle, th->font_body, subcol);
/* 选中描边(轮廓);禁用整体变淡。 */
if (sel) meow_ui_draw_rect_round(cell_x, cell_y, cw, chh, th->radius_card, th->primary);
/* 选中描边(轮廓);禁用卡内变淡(圆角 α,防圆角外叠出灰方块)。 */
if (sel) meow_ui_draw_rect_round(cx, cy, cw, chh, th->radius_card, th->primary);
if (!it->enabled)
meow_ui_fill_rect_alpha(cell_x, cell_y, cw, chh, th->onSurface, 90);
meow_gfx_fill_round_alpha(cx, cy, cw, chh, th->radius_card, th->onSurface, 90);
}
void meow_lazygrid_draw(meow_lazygrid *g, const meow_theme *th)
+314
View File
@@ -0,0 +1,314 @@
/*
* meow_lyric.c — SDK 通用歌词组件实现(见 meow_lyric.h 的契约说明)。
*
* 本 TU 为 ncm_app/app/logic/lyric.c(解析/清空/查询)+ player.c 的 lyr_wrap_lines(折行)的
* 通用化上收:去掉 ncm 特有命名(NCM_LYRIC_LINES / LRC_MAX_TEXT / lyrical 前缀),改用
* meow_lyric_* 前缀与 SDK 命名;行上限由编译期宏变为「静态硬上限 + 运行时可配置」;
* 时间戳存储用 int64_t(跨 32/64 位宿主一致);宽测量由使用方注入回调(不解耦渲染层)。
* 逻辑(LRC 时间标签解析、时间升序排序、翻译按 timestamp 合并、字面 \n 当换行、
* 流式有界、超宽折行中西文混排)与 ncm 逐行保持一致;改动仅限命名/接口/上限可配。
*
* 线程安全:单把 pthread 互斥锁(函数级,写侧整段持锁、读侧每次持锁,无嵌套;
* 内部清空/解析用 nolock 私有版避免自锁死)。与 ncm lyric.c 的机制完全相同。
*
* 内存纪律:静态存储(行数组 + 文本缓冲)零 malloc;仅 meow_lyric_parse 排序时在**堆**上
* 申请瞬态索引/拷贝数组(ncm 实测:600 行×192 字节的临时数组若放栈会占 ~227KB,超出后台
* 线程 128KB 栈 → 栈溢出 SIGSEGV;放堆按 s_n 计、用后即 free)。任一 malloc 失败保留未排序
* (不崩,查询仍可用)。
*/
#include "meow_lyric.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
/* ---- 运行时状态(静态,解析/查询全程零 malloc;仅排序时瞬态堆)---- */
static int64_t s_t[MEOW_LYRIC_LINES_MAX]; /* 每行时间戳(ms) */
static char s_txt[MEOW_LYRIC_LINES_MAX][MEOW_LYRIC_TEXT_MAX]; /* 原文 */
static char s_tl[MEOW_LYRIC_LINES_MAX][MEOW_LYRIC_TEXT_MAX]; /* 翻译 */
static int s_n = 0; /* 有效行数 */
static int g_max_lines = MEOW_LYRIC_LINES_MAX; /* 运行时可配行上限(默认 600) */
static pthread_mutex_t g_lyric_mu = PTHREAD_MUTEX_INITIALIZER;
/* 清空(nolock,内部用;调用方须已持锁)。 */
static void meow_lyric_clear_nolock(void)
{
s_n = 0;
memset(s_t, 0, sizeof(s_t));
memset(s_txt, 0, sizeof(s_txt));
memset(s_tl, 0, sizeof(s_tl));
}
/* 复制一行歌词正文 n 字节到 dst,把「字面反斜杠 n/r」序列(服务器用 \n 把多行文本塞进一行,
* 字面 \n 渲染成豆腐块/凌乱)替换为空格;顺带把 \r\n / \n\r 折叠为单空格。有界 ≤max。
* 语义与 ncm copy_lrc_text 一致。 */
static void meow_lyric_copy_text(char *dst, const char *src, size_t n, size_t max)
{
size_t oi = 0, si = 0;
while (si < n && oi < max - 1) {
if (src[si] == '\\' && si + 1 < n && (src[si + 1] == 'n' || src[si + 1] == 'r')) {
dst[oi++] = ' ';
si += 2;
/* \r\n / \n\r 成对 → 只留一个空格(跳过紧随的互补转义,防双空格) */
if (si + 1 < n && src[si] == '\\' &&
((src[si + 1] == 'r' && src[si - 1] == 'n') ||
(src[si + 1] == 'n' && src[si - 1] == 'r')))
si += 2;
} else {
dst[oi++] = src[si++];
}
}
dst[oi] = 0;
}
/* 解析单个 [mm:ss.ff] 时间标签为毫秒。返回 1=合法时间标签(*out_ms、*next=']' 之后);
* 0=非时间标签(*next=']' 之后;NULL=无闭合 ']',调用方按残缺处理)。
* 小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
* 网易云 LRC 毫秒)、>3 位截断到毫秒。 */
static int meow_lyric_parse_tag(const char *p, int64_t *out_ms, const char **next)
{
const char *q = p + 1; /* 跳过 '[' */
int m = 0, md = 0, s = 0, sd = 0;
while (*q >= '0' && *q <= '9') { m = m * 10 + (*q - '0'); md++; q++; }
if (md == 0 || *q != ':') goto notime;
q++;
while (*q >= '0' && *q <= '9') { s = s * 10 + (*q - '0'); sd++; q++; }
if (sd == 0) goto notime;
long frac = 0; int fd = 0; long scale = 10;
if (*q == '.') {
q++;
while (*q >= '0' && *q <= '9') { frac = frac * 10 + (*q - '0'); fd++; q++; }
if (fd == 1) scale = 100;
else if (fd == 2) scale = 10;
else if (fd >= 3) { scale = 1; while (fd > 3) { frac /= 10; fd--; } } /* >3 位截断到 ms */
else scale = 0; /* 孤立 '.' 无数字:忽略小数 */
}
if (*q != ']') goto notime;
*out_ms = (int64_t)m * 60000L + (int64_t)s * 1000L + frac * scale;
*next = q + 1;
return 1;
notime:
{
const char *r = strchr(p, ']');
*next = r ? r + 1 : NULL;
return 0;
}
}
/* 解析一批 LRC 行(原文 is_trans=0 / 翻译 is_trans=1)。翻译按 first_ms 匹配到已有原文行合并。
* 行上限取 g_max_lines;超出的原文行丢弃。不持锁(调用方已持锁)。 */
static void meow_lyric_parse_lines(const char *lrc, int is_trans)
{
if (!lrc) return;
const char *p = lrc;
while (*p) {
while (*p && *p != '[') p++;
if (!*p) break;
/* 解析一个或多个时间标签 */
int have = 0;
int64_t first_ms = 0;
while (*p == '[') {
int64_t ms;
const char *after;
if (meow_lyric_parse_tag(p, &ms, &after)) {
if (!have) { first_ms = ms; have = 1; }
p = after;
} else {
/* 非时间标签([ti:..] 等)——跳到行尾 */
if (!after) return; /* 无闭合 ']':残缺文件 */
p = after;
break;
}
}
if (have) {
/* 正文 = 到行尾(去 \r\n) */
const char *e = p;
while (*e && *e != '\n' && *e != '\r') e++;
size_t len = (size_t)(e - p);
if (len > MEOW_LYRIC_TEXT_MAX - 1) len = MEOW_LYRIC_TEXT_MAX - 1;
if (is_trans) {
/* 翻译行:按 first_ms 匹配到已有原文行 */
for (int i = 0; i < s_n; i++) {
if (s_t[i] == first_ms) {
meow_lyric_copy_text(s_tl[i], p, len, MEOW_LYRIC_TEXT_MAX);
break;
}
}
} else {
if (s_n < g_max_lines) {
s_t[s_n] = first_ms;
meow_lyric_copy_text(s_txt[s_n], p, len, MEOW_LYRIC_TEXT_MAX);
s_tl[s_n][0] = 0;
s_n++;
}
}
}
while (*p && *p != '\n') p++; /* 跳到下一行 */
if (*p == '\n') p++;
}
}
/* qsort 比较:按时间戳升序(用索引数组。s_t 为静态数组,比较器直接读它)。 */
static int meow_lyric_cmp(const void *a, const void *b)
{
const int *ia = (const int *)a, *ib = (const int *)b;
if (s_t[*ia] < s_t[*ib]) return -1;
if (s_t[*ia] > s_t[*ib]) return 1;
return 0;
}
/* 折行宽测量的兜底:字节数近似(仅当使用方未注入 width 时;非像素精确)。
* 保证 meow_lyric_wrap 在无渲染引擎下仍行为确定(maxw 作字节预算,仍按中西文断行)。 */
static int meow_lyric_default_width(const char *u, int size, void *ctx)
{
(void)size; (void)ctx;
return (int)strlen(u);
}
void meow_lyric_clear(void)
{
pthread_mutex_lock(&g_lyric_mu);
meow_lyric_clear_nolock();
pthread_mutex_unlock(&g_lyric_mu);
}
int meow_lyric_parse(const char *lrc, const char *tlrc)
{
pthread_mutex_lock(&g_lyric_mu);
meow_lyric_clear_nolock();
meow_lyric_parse_lines(lrc, 0);
meow_lyric_parse_lines(tlrc, 1);
/* 按时间排序(用索引数组)。临时数组放**堆**上:`char [600][192]`×2 + `int64[600]`
* 若放栈会占 ~227KB,超过后台线程 128KB 栈 → 栈溢出 SIGSEGV。堆上按 s_n 计
* (≤600 行),峰值 ~230KB 瞬态、用后即 free。 */
if (s_n > 0) {
int *idx = (int *)malloc(sizeof(int) * (size_t)s_n);
int64_t *ttmp = (int64_t *)malloc(sizeof(int64_t) * (size_t)s_n);
char (*txt)[MEOW_LYRIC_TEXT_MAX] =
(char (*)[MEOW_LYRIC_TEXT_MAX])malloc((size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
char (*tl)[MEOW_LYRIC_TEXT_MAX] =
(char (*)[MEOW_LYRIC_TEXT_MAX])malloc((size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
if (idx && ttmp && txt && tl) {
for (int i = 0; i < s_n; i++) idx[i] = i;
qsort(idx, (size_t)s_n, sizeof(int), meow_lyric_cmp);
memcpy(ttmp, s_t, sizeof(int64_t) * (size_t)s_n);
memcpy(txt, s_txt, (size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
memcpy(tl, s_tl, (size_t)MEOW_LYRIC_TEXT_MAX * (size_t)s_n);
for (int i = 0; i < s_n; i++) {
s_t[i] = ttmp[idx[i]];
memcpy(s_txt[i], txt[idx[i]], (size_t)MEOW_LYRIC_TEXT_MAX);
memcpy(s_tl[i], tl[idx[i]], (size_t)MEOW_LYRIC_TEXT_MAX);
}
}
free(idx); free(ttmp); free(txt); free(tl); /* 任一 malloc 失败:保留未排序(不崩) */
}
int result = s_n;
pthread_mutex_unlock(&g_lyric_mu);
return result;
}
int meow_lyric_count(void)
{
pthread_mutex_lock(&g_lyric_mu);
int n = s_n;
pthread_mutex_unlock(&g_lyric_mu);
return n;
}
int meow_lyric_line_at(int64_t ms)
{
pthread_mutex_lock(&g_lyric_mu);
int line = 0;
for (int i = 0; i < s_n; i++) {
if (s_t[i] <= ms) line = i;
else break;
}
pthread_mutex_unlock(&g_lyric_mu);
return line;
}
const char *meow_lyric_line_text(int i)
{
pthread_mutex_lock(&g_lyric_mu);
const char *p = (i < 0 || i >= s_n) ? "" : s_txt[i];
pthread_mutex_unlock(&g_lyric_mu);
return p;
}
const char *meow_lyric_line_tl(int i)
{
pthread_mutex_lock(&g_lyric_mu);
const char *p = (i < 0 || i >= s_n) ? "" : s_tl[i];
pthread_mutex_unlock(&g_lyric_mu);
return p;
}
int64_t meow_lyric_line_time(int i)
{
pthread_mutex_lock(&g_lyric_mu);
int64_t t = (i < 0 || i >= s_n) ? -1 : s_t[i];
pthread_mutex_unlock(&g_lyric_mu);
return t;
}
void meow_lyric_set_max_lines(int max)
{
pthread_mutex_lock(&g_lyric_mu);
if (max < 1) max = 1;
if (max > MEOW_LYRIC_LINES_MAX) max = MEOW_LYRIC_LINES_MAX;
g_max_lines = max;
pthread_mutex_unlock(&g_lyric_mu);
}
int meow_lyric_wrap(const char *txt, int size, int maxw,
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows,
meow_lyric_width_fn width, void *ctx)
{
meow_lyric_width_fn w = width ? width : meow_lyric_default_width;
if (!txt || !txt[0] || !seg || max_rows <= 0) {
if (seg) seg[0][0] = 0;
return 1;
}
int n = 0;
size_t len = strlen(txt), pos = 0;
while (pos < len && n < max_rows) {
size_t start = pos;
int last_ws = -1; /* 本行最后一个空格(相对 start 的字节偏移) */
int overflow = 0;
while (pos < len) {
unsigned char c = (unsigned char)txt[pos];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
if (pos + (size_t)ch > len) ch = (int)(len - pos);
size_t cand = (pos + (size_t)ch) - start;
size_t cp = cand < MEOW_LYRIC_SEG_SZ - 1 ? cand : MEOW_LYRIC_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
if (w(seg[n], size, ctx) > maxw && cand > 0) { overflow = 1; break; }
if (txt[pos] == ' ' || txt[pos] == '\t') last_ws = (int)(pos - start);
pos += (size_t)ch;
}
if (overflow) {
size_t linelen = (last_ws > 0) ? (size_t)last_ws : (pos - start);
if (linelen == 0) { /* 单字符超宽(罕见):至少放一个字符防死循环 */
unsigned char c = (unsigned char)txt[start];
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
linelen = (size_t)ch;
}
size_t cp = linelen < MEOW_LYRIC_SEG_SZ - 1 ? linelen : MEOW_LYRIC_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
pos = start + linelen;
if (last_ws > 0) {
size_t sp = start + (size_t)last_ws + 1;
if (sp > pos) pos = sp; /* 跳过断点后的一个空格 */
}
n++;
} else {
size_t cp = (len - start) < MEOW_LYRIC_SEG_SZ - 1 ? (len - start) : MEOW_LYRIC_SEG_SZ - 1;
memcpy(seg[n], txt + start, cp); seg[n][cp] = 0;
n++;
break;
}
}
if (n == 0) { seg[0][0] = 0; n = 1; }
return n;
}
+82
View File
@@ -0,0 +1,82 @@
/*
* meow_lyric.h — MeowOS SDK 通用歌词组件(LRC 时间轴解析 / 存取 / 查询 + 折行辅助)。
*
* 定位:把网易云 ncm_app 的歌词解析/查询能力(app/logic/lyric.c 的 parse/clear/count/
* line_at/line_text/line_tl + copy_lrc_text 的「字面 \n 当换行」语义 + player.c 的
* lyr_wrap_lines 折行)**通用化上收到 SDK**,供 launcher / ncm 等任意 App 复用。
* 本头为**独立公共头**(仿 meow_ui_view.h 模式:不随 meow_ui.h 伞头发布,App 按需
* `#include "meow_lyric.h"`),命名与 SDK 规范一致:API 一律 `meow_lyric_*`。
*
* 输入:标准 LRC 行 `[mm:ss.xx]正文`。lrc=原文,tlrc=翻译。解析把带时间戳的行按时间升序
* 排序存 timestamp(ms)/text;翻译按相同 timestamp 合并到对应原文行(存 tl),无匹配的翻译行
* 忽略。流式、有界:行数上限默认 MEOW_LYRIC_LINES_MAX(600),运行时可经
* meow_lyric_set_max_lines 下调;单行文本上限 MEOW_LYRIC_TEXT_MAX(192)字节。
*
* 服务器字面序列:网易云服务器把多行文本用字面 `\n`/`\r` 塞进一行(渲染成豆腐块/凌乱),
* 解析时按空格替换(`\r\n`/`\n\r` 折成单空格),语义与 ncm copy_lrc_text 一致。
*
* 线程安全(单写多读,机制与 ncm lyric.c 相同的交错安全):数据由后台线程写
* (meow_lyric_parse / meow_lyric_clear / meow_lyric_set_max_lines),UI 线程读
* (meow_lyric_count / line_at / line_text / line_tl / line_time)。全部函数内部持同一把
* pthread 互斥锁,函数级粒度,写侧整段持锁、读侧每次访问持锁,无嵌套(清空/解析用 nolock
* 私有版,避免二次取锁自锁死)。**读接口返回的指针指向静态存储,在下次 meow_lyric_parse /
* meow_lyric_clear 之前有效**;调用方不得跨重建持有。这是与 ncm 完全相同的「指针在下次写前
* 有效」契约。
*
* 依赖:纯 libc(stdlib/string/stdio)+ pthread,不依赖其它 SDK 对象(不含 meow_log,便于
* launcher 等非 meow_ui.a 使用方独立链入)。宿主 gcc 可直接编译单测。
*
* 时间戳小数位自适应:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,
* 网易云 LRC 毫秒)、>3 位截断到毫秒 —— 避免把网易云 3 位毫秒当 2 位厘秒解析导致时间放大。
*/
#ifndef MEOW_LYRIC_H
#define MEOW_LYRIC_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ---- 容量 / 静态存储 ---- */
#define MEOW_LYRIC_LINES_MAX 600 /* 静态行数组硬上限(对齐 ncm NCM_LYRIC_LINES) */
#define MEOW_LYRIC_TEXT_MAX 192 /* 单行文本(原文/翻译)字节上限(对齐 ncm LRC_MAX_TEXT) */
#define MEOW_LYRIC_SEG_SZ 256 /* 折行单段缓冲字节上限(对齐 ncm LYR_SEG_SZ) */
/* ---- 折行宽测量回调 ----
* 由使用方注入(歌词视图传 meow_ui_text_width 的包装;宿主测传自己的测量函数)。
* 返回 utf8 段在字号 size 下的宽度(px)。NULL 时回退为「字节数近似」(非像素精确,仅兜底)。 */
typedef int (*meow_lyric_width_fn)(const char *utf8, int size, void *ctx);
/* ---- 解析 / 清空 ----
* meow_lyric_parse:重置并解析 lrc(原文)+ tlrc(翻译),按时间升序排序后返回歌词行数
* (=meow_lyric_count 的值;<- 解析失败/空输入返回 0)。翻译按相同时间戳合并到对应原文行。
* meow_lyric_clear:清空全部数据(下次 read 返回空)。 */
void meow_lyric_clear(void);
int meow_lyric_parse(const char *lrc, const char *tlrc);
/* ---- 查询(全部线程安全;line_text/tl 返回静态串,越界返回 "")---- */
int meow_lyric_count(void);
int meow_lyric_line_at(int64_t ms); /* 当前行下标:last timestamp<=ms;首戳前=0;末戳后=n-1 */
const char *meow_lyric_line_text(int i); /* 第 i 行原文;越界 "" */
const char *meow_lyric_line_tl(int i); /* 第 i 行翻译;越界/无翻译 "" */
int64_t meow_lyric_line_time(int i); /* 第 i 行时间戳(ms);越界 -1(供「点行跳转」seek) */
/* ---- 行上限配置 ----
* 缺省 MEOW_LYRIC_LINES_MAX(600)。可下调至 [1, MEOW_LYRIC_LINES_MAX];下一次 parse 起生效
* (解析行数超过上限时超出的行被截断丢弃)。 */
void meow_lyric_set_max_lines(int max);
/* ---- 折行辅助(上收 ncm lyr_wrap_lines,非线程相关、纯函数)----
* 把 txt 按「词/字符」折成 ≤max_rows 段,每段宽度 ≤maxw px(宽测由 width 回调给出;中文按字符、
* 英文按词断行)。段写入调用方 seg[][MEOW_LYRIC_SEG_SZ](seg 须能容纳 max_rows 个段),
* 返回段数(≥1;txt 空/超短返回 1 段空串)。单字符超宽时强制容纳该字符,防止死循环。 */
int meow_lyric_wrap(const char *txt, int size, int maxw,
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows,
meow_lyric_width_fn width, void *ctx);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_LYRIC_H */
+362
View File
@@ -0,0 +1,362 @@
/*
* meow_lyric_view.c — SDK 通用歌词页渲染控件实现(见 meow_lyric_view.h 的契约说明)。
*
* 上收自 ncm_app/app/ui/player.c:lyr_layout(活动行块中心偏上锚定、上方 ≥LYR_ABOVE_MIN 行已过、
* 下填满、总 ≤LYR_MAX_ROWS)+ plr_draw_lyrics(活动行纯白、上下行逐级灰阶、翻译紧跟随不叠行,
* 块高含翻译行是 agent-380 修好 s_lyr_bh 的关键)+ lyrics_line_tap(命中=布局逐块扫描)。折行复用
* 批1 meow_lyric_wrap(=ncm lyr_wrap_lines 的通用化),宽度测量经注入的 meow_lyric_width_fn。
*
* 控件形态:W_CUSTOM 自绘画布(meow_widget_custom_create),render_cb/hit_cb 由本 TU 实现。
* 布局(lv_compute_layout)计算结果缓存在 lv->vis[0..vis_n),绘制/命中/公开查询共用,三者一致。
*
* 内存纪律:per-instance struct(calloc);布局数组定长 [MEOW_LYRIC_VIEW_MAX_ROWS];折行复用缓冲
* 一块 [MEOW_LYRIC_VIEW_MAX_ROWS][MEOW_LYRIC_SEG_SZ](逐条目复用,同 ncm s_lyr_seg 模式);
* 零临时堆分配(meow_lyric_wrap 静态缓冲)。
*/
#include "meow_lyric_view.h"
#include "meow_ui_internal.h" /* meow_widget_rect:widget 为不透明类型,取几何用访问器 */
#include <stdlib.h>
#include <string.h>
/* 折行宽测量的兜底:字节数近似(仅当使用方未注入 width 时;非像素精确)。
* 保证无渲染引擎下行为确定(maxw 作字节预算,仍按中西文断行)——同 meow_lyric_wrap 的 NULL 语义。 */
static int lv_default_width(const char *u, int size, void *ctx)
{
(void)size; (void)ctx;
return (int)strlen(u);
}
/* 逐通道比例缩放(t=0..255,255=原色)。灰度递减用:对 cur 逐级缩灰(对纯白即复现 ncm 灰阶表)。 */
static uint32_t lv_scale_col(uint32_t c, int t)
{
int r = ((int)((c >> 16) & 255) * t) / 255;
int g = ((int)((c >> 8) & 255) * t) / 255;
int b = ((int)( c & 255) * t) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 灰阶比例(距活动行 0..5):0xFFFFFF→{FFFFFF,BBBBBB,929292,808080,6A6A6A,555555}(ncm lyr_gray_tbl)。
* 对非纯白 cur 按同比例缩灰,仍单调变灰。 */
static const int lv_gray_fac[6] = { 255, 187, 146, 128, 106, 85 };
/* 两色线性混合(t=0..255,0=纯 a、255=纯 b)。翻译行融入背景用(无 GPU,逐通道整数混)。 */
static uint32_t lv_mix(uint32_t a, uint32_t b, int t)
{
if (t < 0) t = 0;
if (t > 255) t = 255;
int ar = (a >> 16) & 255, ag = (a >> 8) & 255, ab = a & 255;
int br = (b >> 16) & 255, bg = (b >> 8) & 255, bb = b & 255;
int r = ar + ((br - ar) * t + 128) / 255;
int g = ag + ((bg - ag) * t + 128) / 255;
int bl = ab + ((bb - ab) * t + 128) / 255;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)bl;
}
struct meow_lyric_view {
meow_widget *w;
/* 宽度测量(复用批1 meow_lyric_wrap 的 width_fn 约定;NULL→字节近似兜底) */
meow_lyric_width_fn width;
void *width_ctx;
int64_t pos_ms; /* 当前播放位置(UI 线程写) */
meow_lyric_view_colors colors; /* 用户覆盖(has_colors=1 生效;否则主题默认) */
int has_colors;
meow_lyric_view_layout l; /* 布局参数(内建默认/用户覆盖) */
char empty[64]; /* 无歌词占位文案 */
meow_lyric_seek_fn on_seek;
void *seek_ctx;
/* 布局暂存(每次 lv_compute_layout 重建;UI 线程串行,per-instance 复用) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int vis_n;
char seg[MEOW_LYRIC_VIEW_MAX_ROWS][MEOW_LYRIC_SEG_SZ]; /* 折行复用缓冲(逐条目覆盖) */
};
/* 内建默认布局(对齐 ncm LYR_PAD/SUB_LH/CUR_SZ/OTH_SZ/TL_SZ/MAX_ROWS/ABOVE_MIN/ABOVE_OFF)。 */
static const meow_lyric_view_layout k_def_layout = {
24, 44, 28, 24, 20, MEOW_LYRIC_VIEW_MAX_ROWS, 2, 50
};
static const char k_def_empty[] = "歌词加载中…";
/* 解析生效色:!has_colors 或字段 0/scrim_alpha<0 → 主题默认;否则用覆盖。out 必填。 */
static void lv_resolve_colors(const meow_lyric_view *lv, const meow_theme *th,
meow_lyric_view_colors *out)
{
/* 内建默认(theme 驱动) */
uint32_t scrim = th->scrim;
if (scrim == 0) scrim = th->surfaceDim; /* scrim 缺省为黑:用 surfaceDim 兜底(非纯黑) */
out->scrim = scrim;
out->cur = th->onSurface; /* dark=0xFFFFFF 纯白(活动行高亮) */
out->scrim_alpha = 176;
if (!lv->has_colors) {
out->gray[0] = out->cur;
for (int i = 1; i < 6; i++) out->gray[i] = lv_scale_col(out->cur, lv_gray_fac[i]);
return;
}
/* 用户覆盖(0 字段=沿用内建默认;scrim_alpha<0=沿用默认 176)。
* gray 显式字段优先;未给字段以「解析后 cur」为基准自动递减(对齐头文档
* "gray[1..5]=对 cur 逐级缩灰"——覆盖 cur 后灰阶也应随 cur,而非随主题默认 onSurface)。 */
if (lv->colors.scrim != 0) out->scrim = lv->colors.scrim;
if (lv->colors.cur != 0) out->cur = lv->colors.cur;
if (lv->colors.scrim_alpha >= 0) out->scrim_alpha = lv->colors.scrim_alpha;
out->gray[0] = (lv->colors.gray[0] != 0) ? lv->colors.gray[0] : out->cur;
for (int i = 1; i < 6; i++)
out->gray[i] = (lv->colors.gray[i] != 0) ? lv->colors.gray[i]
: lv_scale_col(out->cur, lv_gray_fac[i]);
}
/* 距活动行 dist 的生效行色(dist 越界 clamp 0..5)。 */
static uint32_t lv_line_color(const meow_lyric_view *lv, const meow_theme *th, int dist)
{
meow_lyric_view_colors c;
lv_resolve_colors(lv, th, &c);
if (dist < 0) dist = 0;
if (dist > 5) dist = 5;
return c.gray[dist];
}
/* 折行封套(width 缺省兜底;越界 max_rows 内 clamp 到缓冲容量)。返回段数(≥1)。 */
static int lv_wrap(const meow_lyric_view *lv, const char *txt, int size, int maxw,
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows)
{
meow_lyric_width_fn w = lv->width ? lv->width : lv_default_width;
if (max_rows > MEOW_LYRIC_VIEW_MAX_ROWS) max_rows = MEOW_LYRIC_VIEW_MAX_ROWS;
int rc = meow_lyric_wrap(txt, size, maxw, seg, max_rows, w, lv->width_ctx);
if (rc > max_rows) rc = max_rows;
return rc;
}
/* 核心布局:按 pos_ms 计算可见条目(vis[0..n)。算法与 ncm lyr_layout 逐行为一致)——
* 活动行块中心锚定在视图中线 - above_off(偏上);上方保底 ≥above_min 行已过;下填满;总 ≤max_rows;
* 块高 bh = 原文折行数 rw + 当前行翻译占 1(其余条目无翻译,bh=rw;翻译不叠行,点行命中一致)。 */
static int lv_compute_layout(meow_lyric_view *lv, int64_t pos_ms, int maxw)
{
int n = meow_lyric_count();
int cur = (n > 0) ? meow_lyric_line_at(pos_ms) : -1;
memset(lv->vis, 0, sizeof(lv->vis));
lv->vis_n = 0;
if (cur < 0 || cur >= n) return 0;
int MAX = lv->l.max_rows;
if (MAX > MEOW_LYRIC_VIEW_MAX_ROWS) MAX = MEOW_LYRIC_VIEW_MAX_ROWS;
if (MAX < 1) MAX = 1;
int rc = lv_wrap(lv, meow_lyric_line_text(cur), lv->l.cur_sz, maxw, lv->seg, MAX);
const char *tl = meow_lyric_line_tl(cur);
int has_tl = (tl && tl[0]) ? 1 : 0;
int rc_tot = rc + has_tl; /* 活动行块总子行数(原文折行 + 翻译行) */
int cap = MAX - rc_tot; /* 活动行之外可用的视觉行数 */
if (cap < 0) cap = 0;
int above_want = (cap * 2) / 5; /* 偏上锚定:约 40% 行给上方 */
if (above_want > cur) above_want = cur; /* 上方行数上限 = 已过行数 */
if (above_want < lv->l.above_min) above_want = (cur >= lv->l.above_min) ? lv->l.above_min : cur;
if (above_want > cap) above_want = cap; /* 总行数 ≤ MAX */
/* 1) 上方(cur-1 向上)前插到头部,保持自顶向下 */
int a_cnt = 0, usedA = 0;
for (int i = cur - 1; i >= 0 && usedA < above_want; i--) {
int rr = lv_wrap(lv, meow_lyric_line_text(i), lv->l.oth_sz, maxw, lv->seg, MAX);
if (usedA + rr > above_want) break;
for (int k = a_cnt; k > 0; k--) lv->vis[k] = lv->vis[k - 1];
lv->vis[0].ent = i; lv->vis[0].rw = rr; lv->vis[0].bh = rr; lv->vis[0].sz = lv->l.oth_sz;
a_cnt++; usedA += rr;
}
/* 2) 当前行 */
lv->vis[a_cnt].ent = cur; lv->vis[a_cnt].rw = rc; lv->vis[a_cnt].bh = rc_tot; lv->vis[a_cnt].sz = lv->l.cur_sz;
/* 3) 下方(cur+1 向下) */
int below_want = cap - usedA;
if (below_want > (n - 1 - cur)) below_want = n - 1 - cur;
int b_cnt = 0, usedB = 0;
for (int i = cur + 1; i < n && usedB < below_want; i++) {
int rr = lv_wrap(lv, meow_lyric_line_text(i), lv->l.oth_sz, maxw, lv->seg, MAX);
if (usedB + rr > below_want) break;
int k = a_cnt + 1 + b_cnt;
lv->vis[k].ent = i; lv->vis[k].rw = rr; lv->vis[k].bh = rr; lv->vis[k].sz = lv->l.oth_sz;
b_cnt++; usedB += rr;
}
int cnt = a_cnt + 1 + b_cnt;
if (cnt == 0) return 0;
/* 4) y 定位:活动行原文块中心 = 视图中线 - above_off(偏上);上方向上、下方向下(块间无缝隙) */
int rect[4]; meow_widget_rect(lv->w, rect);
int anchor = rect[1] + rect[3] / 2 - lv->l.above_off; /* rect[1]=vy, rect[3]=vh */
lv->vis[a_cnt].y = anchor - rc * lv->l.sub_lh / 2;
for (int k = a_cnt + 1; k < cnt; k++)
lv->vis[k].y = lv->vis[k - 1].y + lv->vis[k - 1].bh * lv->l.sub_lh;
for (int k = a_cnt - 1; k >= 0; k--)
lv->vis[k].y = lv->vis[k + 1].y - lv->vis[k].bh * lv->l.sub_lh;
lv->vis_n = cnt;
return cnt;
}
/* 自绘回调:scrim 底 + 滚动歌词(活动行纯白偏上锚定 + 上下行逐级灰阶)+ 翻译行(不叠行)+ 空态占位。 */
static void lv_render(meow_widget *w, const meow_theme *th, void *ctx)
{
meow_lyric_view *lv = (meow_lyric_view *)ctx;
int rect[4]; meow_widget_rect(w, rect);
int x = rect[0], vy = rect[1], W = rect[2], H = rect[3];
meow_lyric_view_colors col;
lv_resolve_colors(lv, th, &col);
/* 背景 scrim(半透明叠在封面之上;scrim_alpha==0=不画,App 已自绘叠加层)。 */
if (col.scrim_alpha > 0) meow_ui_fill_rect_alpha(x, vy, W, H, col.scrim, col.scrim_alpha);
int n = meow_lyric_count();
int cur = (n > 0) ? meow_lyric_line_at(lv->pos_ms) : -1;
int maxw = W - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
uint32_t bgcol = col.scrim; /* 翻译行融入背景(渐隐) */
for (int k = 0; k < cnt; k++) {
int i = lv->vis[k].ent, rows = lv->vis[k].rw, sz = lv->vis[k].sz, vsy = lv->vis[k].y;
int is_cur = (i == cur);
int dist = is_cur ? 0 : ((i - cur) < 0 ? (cur - i) : (i - cur));
uint32_t c = lv_line_color(lv, th, dist);
int rr = lv_wrap(lv, meow_lyric_line_text(i), sz, maxw, lv->seg, rows);
if (rr > rows) rr = rows;
for (int r = 0; r < rr; r++)
meow_ui_text(x + lv->l.pad, vsy + r * lv->l.sub_lh, lv->seg[r], sz, c);
if (is_cur) {
const char *tl = meow_lyric_line_tl(i);
if (tl && tl[0])
meow_ui_text(x + lv->l.pad, vsy + rows * lv->l.sub_lh + 2, tl,
lv->l.tl_sz, lv_mix(0xFFFFFF, bgcol, 120));
}
}
if (n == 0)
meow_ui_text(x + lv->l.pad, vy + H / 2 - 10, lv->empty, 20, col.cur);
}
/* 命中回调:点行 seek(用与绘制一致的布局逐块扫描;块外就近取整)。
* 命中某行 → 存 last 行 + 回调 on_seek(ms)(若有),返回 1=消费;否则 0。 */
static int lv_hit_cb(meow_widget *w, int gx, int gy, void *ctx)
{
meow_lyric_view *lv = (meow_lyric_view *)ctx;
int rect[4]; meow_widget_rect(w, rect);
int rx = rect[0], ry = rect[1], rw = rect[2], rh = rect[3];
/* 视图矩形内含判定(含边缘;点必须在控件内)。 */
if (gx < rx || gx >= rx + rw || gy < ry || gy >= ry + rh) return 0;
int n = meow_lyric_count();
if (n <= 0) return 0;
int maxw = rw - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
if (cnt <= 0) return 0;
int line = -1;
for (int k = 0; k < cnt; k++) {
int yb = lv->vis[k].y;
int yh = lv->vis[k].bh * lv->l.sub_lh; /* 块高含当前行翻译,点翻译行归本行 */
if (gy >= yb && gy < yb + yh) { line = lv->vis[k].ent; break; }
}
if (line < 0) /* 落在可见块之外 → 就近取整(顶部/底部可见行) */
line = (gy < lv->vis[0].y) ? lv->vis[0].ent : lv->vis[cnt - 1].ent;
if (lv->on_seek) {
int64_t t = meow_lyric_line_time(line);
if (t >= 0) lv->on_seek(t, lv->seek_ctx);
}
return 1;
}
/* ---- 生命周期 ---- */
meow_lyric_view *meow_lyric_view_create(int x, int y, int w, int h,
meow_lyric_width_fn width, void *width_ctx)
{
meow_lyric_view *lv = (meow_lyric_view *)calloc(1, sizeof *lv);
if (!lv) return NULL;
lv->w = meow_widget_custom_create(x, y, w, h, lv_render, lv_hit_cb, lv);
if (!lv->w) { free(lv); return NULL; }
lv->width = width;
lv->width_ctx = width_ctx;
lv->l = k_def_layout;
memcpy(lv->empty, k_def_empty, sizeof k_def_empty);
return lv;
}
void meow_lyric_view_free(meow_lyric_view *lv)
{
if (!lv) return;
if (lv->w) meow_widget_free(lv->w);
free(lv);
}
meow_widget *meow_lyric_view_widget(const meow_lyric_view *lv)
{
return lv ? lv->w : NULL;
}
void meow_lyric_view_set_pos(meow_lyric_view *lv, int64_t pos_ms)
{
if (lv) lv->pos_ms = pos_ms;
}
void meow_lyric_view_set_colors(meow_lyric_view *lv, const meow_lyric_view_colors *c)
{
if (!lv) return;
if (!c) { lv->has_colors = 0; return; }
lv->colors = *c;
lv->has_colors = 1;
}
void meow_lyric_view_set_layout(meow_lyric_view *lv, const meow_lyric_view_layout *ls)
{
if (!lv) return;
lv->l = ls ? *ls : k_def_layout;
}
void meow_lyric_view_set_empty(meow_lyric_view *lv, const char *txt)
{
if (!lv) return;
if (!txt || !txt[0]) { lv->empty[0] = 0; return; }
strncpy(lv->empty, txt, sizeof lv->empty - 1);
lv->empty[sizeof lv->empty - 1] = 0;
}
void meow_lyric_view_set_on_seek(meow_lyric_view *lv, meow_lyric_seek_fn cb, void *ctx)
{
if (!lv) return;
lv->on_seek = cb;
lv->seek_ctx = ctx;
}
/* ---- 查询 ---- */
uint32_t meow_lyric_view_cur_color(const meow_lyric_view *lv)
{
if (!lv) return 0;
return lv_line_color(lv, meow_theme_get(), 0);
}
uint32_t meow_lyric_view_line_color(const meow_lyric_view *lv, int dist)
{
if (!lv) return 0;
return lv_line_color(lv, meow_theme_get(), dist);
}
int meow_lyric_view_hit_line(meow_lyric_view *lv, int x, int y)
{
if (!lv || !lv->w) return -1;
int rect[4]; meow_widget_rect(lv->w, rect);
int rx = rect[0], ry = rect[1], rw = rect[2], rh = rect[3];
if (x < rx || x >= rx + rw || y < ry || y >= ry + rh) return -1;
int n = meow_lyric_count();
if (n <= 0) return -1;
int maxw = rw - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, lv->pos_ms, maxw);
if (cnt <= 0) return -1;
for (int k = 0; k < cnt; k++) {
int yb = lv->vis[k].y, yh = lv->vis[k].bh * lv->l.sub_lh;
if (y >= yb && y < yb + yh) return lv->vis[k].ent;
}
return (y < lv->vis[0].y) ? lv->vis[0].ent : lv->vis[cnt - 1].ent;
}
int meow_lyric_view_visible(meow_lyric_view *lv, int64_t pos_ms,
meow_lyric_view_vis *out, int cap)
{
if (!lv || !lv->w) return 0;
int rect[4]; meow_widget_rect(lv->w, rect);
int maxw = rect[2] - 2 * lv->l.pad; if (maxw < 1) maxw = 1;
int cnt = lv_compute_layout(lv, pos_ms, maxw);
if (out && cnt > 0) {
int n = (cnt < cap) ? cnt : cap;
for (int i = 0; i < n; i++) out[i] = lv->vis[i];
}
return cnt;
}
+110
View File
@@ -0,0 +1,110 @@
/*
* meow_lyric_view.h — MeowOS SDK 通用歌词页渲染控件(可复用,供 launcher Now Playing 与 ncm 共用)。
*
* 定位:把 ncm_app/app/ui/player.c 已成熟(agent-380 修好 s_lyr_bh)的歌词页布局上收成 SDK 控件。
* 能力对齐 ncm 歌词页:活动行纯白(居中偏上锚定)、上下行按行距逐级灰阶、翻译行紧跟原文行不叠行
* (块高含翻译行)、超宽折行(中西文兼容)、上方保底 ≥above_min 行已过、总视觉行数 ≤max_rows、
* 点行 seek 命中与绘制一致(同一布局函数)。
*
* 数据来源:**绑定批1 meow_lyric**(解析/查询 API:meow_lyric_count/line_at/line_text/line_tl/line_time,
* 全局单例,线程安全)+ 播放位置(meow_lyric_view_set_pos)。宽度测量用回调注入
* (`meow_lyric_width_fn`,包 meow_ui_text_width 的包装;宿主测试传自己的测量函数)。
*
* 控件形态:内部持一个 `meow_widget`(W_CUSTOM 自定义画布),经 meow_lyric_view_widget() 取出供
* meow_widget_draw/hit/注册;render_cb/hit_cb 都由本控件自绘/自判。App 也可只用纯查询接口
* (meow_lyric_view_visible/hit_line)在自绘流程里复用布局逻辑。
*
* 主题:用 SDK 全局 meow_theme 为默认(明暗背景 scrim、活动行 onSurface=dark 纯白、灰阶递减),
* 为批5「封面取色动态主题」预留 meow_lyric_view_set_colors 覆盖入口(字段 0=仍用主题默认)。
*
* 依赖:纯 SDK(meow_ui.h + meow_lyric.h),不引任何 ncm 私有符号;可链 meow_ui.a 使用,
* 亦可宿主 gcc 单测(同 test_now_playing_widgets 链法)。本头为**独立公共头**(仿 meow_ui_view.h
* 模式,不随 meow_ui.h 伞头发布),App 显式 `#include "meow_lyric_view.h"`。
*
* 命名:API 一律 `meow_lyric_view_*`(对齐 SDK 规范);控件内布局/绘制/命中共享同一
* meow_lyric_view_visible 计算,保证「点行 seek 命中与绘制一致」。
*/
#ifndef MEOW_LYRIC_VIEW_H
#define MEOW_LYRIC_VIEW_H
#include "meow_ui.h" /* meow_widget / meow_theme / meow_ui_text 等 */
#include "meow_lyric.h" /* meow_lyric_width_fn + 行查询 API(批1) */
#ifdef __cplusplus
extern "C" {
#endif
typedef struct meow_lyric_view meow_lyric_view;
/* 可见层数硬上限(对齐 ncm LYR_MAX_ROWS;数组按此定容,运行时 layout.max_rows 不得越此)。 */
#define MEOW_LYRIC_VIEW_MAX_ROWS 9
/* ---- 可见条目(布局/命中/绘制共用,宿主可测)----
* ent=歌词行下标;rw=原文折行段数;bh=块高(子行数)=rw + 当前行翻译占 1(其余条目 bh=rw);
* sz=该条目字号;y=块顶 y(绝对屏幕坐标)。 */
typedef struct meow_lyric_view_vis {
int ent;
int rw;
int bh;
int sz;
int y;
} meow_lyric_view_vis;
/* ---- 主题色覆盖(批5 封面取色动态主题预留)----
* 全部字段:0(scrim_alpha<0)= 用 meow_theme 内建默认;显式值 = 覆盖。
* 默认映射:scrim=th->scrim(黑叠加在封面之上,alpha 176);cur=th->onSurface(dark=0xFFFFFF 纯白);
* gray[0]=cur,gray[1..5]=对 cur 逐级缩灰(对纯白即 ncm 的 {BBBBBB,929292,808080,6A6A6A,555555})。
* scrim_alpha==0 = 不画 scrim(App 已自绘封面/叠加层时用)。 */
typedef struct meow_lyric_view_colors {
uint32_t scrim; /* 背景 scrim 色(半透明叠加在封面之上) */
uint32_t cur; /* 活动行色(默认 th->onSurface;dark 下=纯白) */
uint32_t gray[6]; /* 距活动行 0..5 灰阶;gray[0]=cur。字段 0=自动递减 */
int scrim_alpha; /* scrim 透明度 0-255;<0=默认(176);0=不画 scrim */
} meow_lyric_view_colors;
/* ---- 布局参数(对齐 ncm;NULL=内建默认)---- */
typedef struct meow_lyric_view_layout {
int pad; /* 左边距(默认 24) */
int sub_lh; /* 子行高(默认 44) */
int cur_sz; /* 活动行字号(默认 28) */
int oth_sz; /* 上下行字号(默认 24) */
int tl_sz; /* 当前行翻译字号(默认 20) */
int max_rows; /* 可见视觉行数上限(默认 9;≤MEOW_LYRIC_VIEW_MAX_ROWS) */
int above_min; /* 活动行上方保底已过行数(默认 2) */
int above_off; /* 活动行块中心相对视图中线偏上 px(默认 50,需求3 偏上锚定) */
} meow_lyric_view_layout;
/* 点行 seek 回调:meow_lyric_view_set_on_seek 设置;命中某行后回调 line_time(ms)。 */
typedef void (*meow_lyric_seek_fn)(int64_t ms, void *ctx);
/* ---- 生命周期 ---- */
meow_lyric_view *meow_lyric_view_create(int x, int y, int w, int h,
meow_lyric_width_fn width, void *width_ctx);
void meow_lyric_view_free(meow_lyric_view *lv);
/* 底层 W_CUSTOM meow_widget:App 用它做 draw/hit/注册(launcher 控件模型)。 */
meow_widget *meow_lyric_view_widget(const meow_lyric_view *lv);
/* ---- 数据绑定 ---- */
void meow_lyric_view_set_pos(meow_lyric_view *lv, int64_t pos_ms); /* 播放器位置(ms) */
/* ---- 主题/布局/空态 ---- */
void meow_lyric_view_set_colors(meow_lyric_view *lv, const meow_lyric_view_colors *c); /* NULL=回主题默认 */
void meow_lyric_view_set_layout(meow_lyric_view *lv, const meow_lyric_view_layout *ls); /* NULL=回内建默认 */
void meow_lyric_view_set_empty(meow_lyric_view *lv, const char *txt); /* 无歌词占位文案;NULL=默认 */
/* 点行 seek(widget hit 命中时回调;App 也可只用 meow_lyric_view_hit_line 自行 seek)。 */
void meow_lyric_view_set_on_seek(meow_lyric_view *lv, meow_lyric_seek_fn cb, void *ctx);
/* ---- 查询 ——
* cur_color / line_color:取该视图生效(主题默认或覆盖)的活动行色 / 距活动行 dist(0..5) 行色。
* visible:按 pos_ms 计算可见条目布局(与绘制/命中同一算法)写入 out[≤cap],返回条目数(≤0=空/无活动行)。
* hit_line:点 (x,y) 命中某歌词行下标(与绘制一致的布局;块外就近取整);未命中返回 -1。 */
uint32_t meow_lyric_view_cur_color(const meow_lyric_view *lv);
uint32_t meow_lyric_view_line_color(const meow_lyric_view *lv, int dist);
int meow_lyric_view_hit_line(meow_lyric_view *lv, int x, int y);
int meow_lyric_view_visible(meow_lyric_view *lv, int64_t pos_ms,
meow_lyric_view_vis *out, int cap);
#ifdef __cplusplus
}
#endif
#endif /* MEOW_LYRIC_VIEW_H */
+40 -5
View File
@@ -75,7 +75,7 @@ static const char *sock_path(void)
}
/* 连接 audiod。**全部有界**:connect 非阻塞 + poll 2s(对端 listen backlog 满/wedge 时
* 阻塞 connect 会把 App 播放线程永久压进内核——真机长流卡死的同款路径);
* 阻塞 connect 会把 App 播放线程永久压进内核——真机长流卡死时 ncm 卡死的同款路径);
* 建连后 SO_SNDTIMEO/RCVTIMEO 2s:audiod 停摆时 write 超时返回错误 → 标记流死,
* App 走「重新 open」恢复路径,绝不永久阻塞。 */
#define AUD_CONN_TMO_MS 2000
@@ -229,7 +229,8 @@ static int fmt_bps(int fmt)
case AUDIO_FMT_S16: return 2;
case AUDIO_FMT_S24:
case AUDIO_FMT_S32:
case AUDIO_FMT_FLOAT: return 4;
case AUDIO_FMT_FLOAT:
case AUDIO_FMT_DOP: return 4; /* DOP=32-bit DoP 容器(16bit DSD+标记+0),同 S32 宽 */
default: return 2;
}
}
@@ -262,7 +263,7 @@ static int send_write_chunk(const uint8_t *p, uint32_t len)
/* ---- 公开 API ---- */
int meow_audio_open(int rate, int ch, int fmt)
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags)
{
/* env 检查:调试直开 */
const char *de = getenv("MEOW_AUDIO_DIRECT");
@@ -270,7 +271,10 @@ int meow_audio_open(int rate, int ch, int fmt)
pthread_mutex_lock(&g_mu);
if (g_has_stream) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_BUSY; }
if (ch > 2 || ch < 1 || rate <= 0 || fmt < AUDIO_FMT_S16 || fmt > AUDIO_FMT_FLOAT || fmt == AUDIO_FMT_FLOAT) {
int dsd = (fmt == AUDIO_FMT_DOP) && (flags & AUDIO_FLAG_DSD);
int fmt_ok = (fmt == AUDIO_FMT_S16 || fmt == AUDIO_FMT_S24 ||
fmt == AUDIO_FMT_S32 || fmt == AUDIO_FMT_DOP);
if (ch > 2 || ch < 1 || rate <= 0 || !fmt_ok || (fmt == AUDIO_FMT_DOP && !dsd)) {
pthread_mutex_unlock(&g_mu);
return MEOW_ERR_INVAL;
}
@@ -297,7 +301,7 @@ int meow_audio_open(int rate, int ch, int fmt)
m_audio_open_t o;
memset(&o, 0, sizeof o);
snprintf(o.app_id, sizeof o.app_id, "%s", getenv("MEOW_LOG_APP") ? getenv("MEOW_LOG_APP") : "app");
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt;
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; o.flags = flags;
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1); /* 单调递增(单个挂起) */
g_pending = req;
g_resp_len = 0;
@@ -322,6 +326,35 @@ int meow_audio_open(int rate, int ch, int fmt)
return (int)g_sid;
}
int meow_audio_open(int rate, int ch, int fmt)
{
return meow_audio_open_ex(rate, ch, fmt, 0);
}
int meow_audio_set_volume(int pct)
{
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
pthread_mutex_lock(&g_mu);
int fd = conn_locked();
if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; }
m_audio_volume_t v; v.pct = (int32_t)pct;
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1);
g_pending = req;
if (send_frame(g_fd, MSG_AUDIO_VOLUME, req, &v, sizeof v) != 0) {
mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO;
}
/* 等 ACK(3s 超时;audiod 停摆时透明失败标记流死) */
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
int rc = 0;
while (g_pending == req) {
if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { rc = MEOW_ERR_TIMEOUT; break; }
}
if (g_resp_type != MSG_ACK) rc = (rc == 0) ? MEOW_ERR_IO : rc;
pthread_mutex_unlock(&g_mu);
return rc;
}
int meow_audio_write(const void *pcm, int frames)
{
if (!pcm || frames <= 0) return 0;
@@ -429,8 +462,10 @@ static int state_text_parse(const char *t, meow_audio_state *o)
if (strncmp(p + 4, "S16", 3) == 0) f = AUDIO_FMT_S16;
else if (strncmp(p + 4, "S24", 3) == 0) f = AUDIO_FMT_S24;
else if (strncmp(p + 4, "S32", 3) == 0) f = AUDIO_FMT_S32;
else if (strncmp(p + 4, "DoP", 3) == 0) f = AUDIO_FMT_DOP;
o->fmt = (uint16_t)f;
}
if ((p = strstr(t, "dsd="))) o->dsd = atoi(p + 4);
if ((p = strstr(t, "written="))) o->written_frames = (uint64_t)strtoull(p + 8, NULL, 10);
if ((p = strstr(t, "played="))) o->played_frames = (uint64_t)strtoull(p + 7, NULL, 10);
if ((p = strstr(t, "underrun="))) o->underrun = (uint32_t)atoi(p + 9);
+3 -3
View File
@@ -100,8 +100,8 @@ static void dlg_calc(meow_dialog *d, const meow_theme *th, dlg_geom *g)
static void dlg_card(meow_dialog *d, const meow_theme *th, const dlg_geom *g)
{
int pad = 24;
meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->surfaceContainerHighest);
meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, th->radius_card, th->outline);
meow_gfx_fill_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest);
meow_ui_draw_rect_round(g->dx, g->dy, g->dw, g->dh, MEOW_SHAPE_EXTRA_LARGE, th->outline);
meow_ui_text(g->dx + pad, g->dy + pad, d->title, th->font_title, th->onSurface);
if (d->type == MEOW_DLG_SELECT) {
@@ -358,7 +358,7 @@ int meow_sheet_open(meow_sheet *s)
meow_ui_modal_capture(); /* M2.71-bug3:拍下背景,关闭即还原(scrim 只叠一层/无残影) */
/* 首次绘制:遮罩 + 底部面板(顶部圆角) */
meow_ui_fill_rect_alpha(0, 0, W, H, 0x000000, 150);
meow_gfx_fill_round(0, py, W, ph, th->radius_card, th->surfaceContainerHighest);
meow_gfx_fill_round(0, py, W, ph, MEOW_SHAPE_EXTRA_LARGE, th->surfaceContainerHighest);
meow_ui_text(pad, py + pad, s->title, th->font_title, th->onSurface);
for (int i = 0; i < s->nitems; i++) {
int ry = py + pad + meow_ui_text_height(th->font_title) + 12 + i * MEOW_MODAL_ROW;
+75 -10
View File
@@ -12,7 +12,7 @@
#include "meow_ui_internal.h"
#include "meow_ui_sys.h"
#include "sb_icons.h"
#include "meow_font.h" /* 公开 SDK 仓内 vendored(include/meow_font.h),不再引主仓 launcher 头 */
#include "meow_font.h"
#include <stdio.h>
#include <stdlib.h>
@@ -830,6 +830,17 @@ int meow_ui_text_height(int 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)
{
@@ -963,13 +974,33 @@ static int statusbar_touched(void)
return 0;
}
/* ---- §10.5 叠加让位自抑(present 层)----
* /tmp/meow_notif_active 存在 = 通知中心叠加会话进行中(launcher 正在 fb 上渲染面板)。
* App 进程(非系统 UI 渲染者)此时不上屏;200ms 节流缓存存在性(别每帧 stat)。
/* ---- §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;
static int overlay_present_suppressed(void)
/* 本进程 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];
@@ -984,7 +1015,29 @@ static int overlay_present_suppressed(void)
unsigned long long now = now_ms();
if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res;
s_last_chk = now;
s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0;
{
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;
}
@@ -995,10 +1048,22 @@ 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 供流,App 播放线程同进程),改由 App 进程在此自抑上屏——绘制照常进 back_buf,
* 只是不 present 到 fb,防 App 重绘穿透 launcher 叠加层。系统 UI 进程(launcher 渲染面板)
* 不抑制。抑制期脏区保留累积(超上限自然升级整帧),标记消失后下一次 present 一次上屏。 */
if (overlay_present_suppressed()) { g_overlay_suppressed++; return; }
* (保 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();
+12 -5
View File
@@ -49,8 +49,12 @@ void meow_img_free(meow_img *im)
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; }
int meow_img_w(const meow_img *im) { return im ? im->w : 0; }
int meow_img_h(const meow_img *im) { return im ? im->h : 0; }
/* 访问器:取 RGBA 像素指针(meow_img 对外 opaque,本函数为唯一像素读入口;供取色/直读像素)。
* NULL 返回 NULL。 */
const unsigned char *meow_img_pixels(const meow_img *im) { return im ? im->rgba : NULL; }
/* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
@@ -70,13 +74,16 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */
float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */
float oys = ((float)im->h - (float)dh * s) * 0.5f;
if (oxs < 0) oxs = 0; if (oys < 0) oys = 0;
if (oxs < 0) oxs = 0;
if (oys < 0) oys = 0;
for (int dy = 0; dy < dh; dy++) {
int syi = (int)((float)dy * s + oys);
if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1;
if (syi < 0) syi = 0;
if (syi >= im->h) syi = im->h - 1;
for (int dx = 0; dx < dw; dx++) {
int sxi = (int)((float)dx * s + oxs);
if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1;
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);
}
+54 -19
View File
@@ -27,6 +27,7 @@
#include "meow_ui_vol.h"
#include "meow_ui_media.h"
#include "meow_ui_view.h" /* M7.0a 缺口#7:把 meow_view_dispatch_event 接入 meow_ui_run */
#include "meow_ui_nav.h" /* 批②:底部上滑回桌面→meow_ui_nav_request_exit(MSG_NAV EXIT→批①保活) */
#include <stdio.h>
#include <stdlib.h>
@@ -57,6 +58,8 @@
#define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */
#define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */
#define HOME_EDGE_H 28 /* 批②:底部起滑区高(自屏底向上;镜像 np_gesture.h NP_HOME_EDGE_H) */
#define HOME_SWIPE 65 /* 批②:底部上滑位移阈值(镜像 np_gesture.h NP_HOME_SWIPE;= SWIPE_THRESH) */
#define MEOW_LONG_PRESS_MS 500u /* §6.3 通用长按默认阈值(ms;meow_ui_set_long_press_ms 可配) */
#define MEOW_LONG_PRESS_SLOP 12 /* §6.3 按下后「仍算按住」的最大位移(px);超此取消长按 */
#define QUEUE_N 128
@@ -71,6 +74,10 @@ void meow_ui_gesture_end(void);
* (路由 meow_ui_sys_notif_pull),不下发给 APP 的 on_swipe(顶部热区为系统保留手势)。
* 与左缘返回(SWIPE_LEFT)/ meow_ui_swipe_hint 互不影响——后者仍全屏生效,详见下方触摸机。 */
#define MEOW_EV_NOTIF_PULL 5
/* 批②:底部热区上滑 → 回桌面(后台保活,走批①链路)。系统手势,a=MEOW_SWIPE_UP(供
* on_swipe 接管、view 树消费),b=位移(px)。定义在 input.c 内部(同 MEOW_EV_NOTIF_PULL
* 未入公共头),宿主单测按同值声明(见 test_meow_ui_home_gesture.c)。 */
#define MEOW_EV_HOME 9
/* 顶部下滑 → 通知中心判定:纵向 DOWN 手势的起点(origin_y)是否落在顶部热区内。
* 热区由 meow_ui_statusbar_hotzone() 给出(非全屏=状态栏高、全屏=MEOW_SBAR_HOTZONE_FS、
@@ -80,6 +87,20 @@ int meow_ui_input_is_notif_pull(int origin_y, int hotzone)
return origin_y >= 0 && origin_y < hotzone;
}
/* 批② 底部热区上滑 → 回桌面(home)判定:镜像 launcher np_gesture.h np_home_gesture 同口径
* (热区 28px / 上移 ≥65px / 竖向主导 / 避开左缘返回列),SDK 侧 fb_h=屏幕高(复用
* meow_ui_height())。供宿主单测直调(判据与触摸状态机 touch_release 共用)。本判定把
* 「底部热区起滑 + 上移超阈值且竖向主导」从普通上滑中剥离——中部上滑/滚动仍归 App
* (LazyColumn/LazyGrid 滚动不受影响),底部热区的横向滑仍走原有左缘返回/横向逻辑。 */
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h)
{
int adx = dx < 0 ? -dx : dx;
if (down_x >= EDGE_W && down_y >= fb_h - HOME_EDGE_H && down_y >= 0 &&
dy <= -HOME_SWIPE && adx < -dy)
return 1;
return 0;
}
static int g_thread_started = 0;
static pthread_t g_thread;
static volatile int g_thread_run = 1;
@@ -140,28 +161,17 @@ static int meow_input_jack_debounce(void)
return g_jack_changed_at && (now - g_jack_changed_at) < 800;
}
/* ---- §10.5 叠加让位:通知中心叠加会话期输入排空(播放中 App 不被 SIGSTOP 的配套)----
/* ---- §10.5 叠加让位:通知中心叠加会话期 / 批①后台常驻期 输入排空(播放中 App 不被 SIGSTOP 的配套)----
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,ncm 播放线程同进程),
* App 输入线程仍在读同一批 evdev 设备(每 fd 各收一份事件副本,与 launcher 互不剥夺)——若不
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发)。
* 故标记存在期:排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作
* 叠加期由 launcher/watchdog 单侧承担)。200ms 节流缓存存在性(与 present 层同口径,别每圈 stat)。 */
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发);
* 批①后台常驻 App 同理:前台 App(如 Hello)时,后台 ncm 的触摸必须被排空(一次交互只响应前台)。
* 判据统一走 meow_ui_overlay_suppressed_active()(内容=前台 app_id;非前台/空=抑制)。抑制期:
* 排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作叠加期由
* launcher/watchdog 单侧承担)。200ms 节流缓存由共享判定承担(与 present 层同口径)。 */
static int overlay_input_suppressed(void)
{
static char s_path[108];
static int s_path_init = 0;
static unsigned long long s_last_chk = 0;
static int s_last_res = 0;
if (!s_path_init) {
const char *e = getenv("MEOW_NOTIF_ACTIVE_FILE"); /* 宿主/单测覆写(进程内只解析一次) */
snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : "/tmp/meow_notif_active");
s_path_init = 1;
}
unsigned long long now = now_ms();
if (s_last_chk && now - s_last_chk < 200) return s_last_res;
s_last_chk = now;
s_last_res = (access(s_path, F_OK) == 0) ? 1 : 0;
return s_last_res;
return meow_ui_overlay_suppressed_active();
}
/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)----
@@ -337,7 +347,16 @@ static void touch_release(void)
}
} else {
int dir = mdy > 0 ? MEOW_SWIPE_DOWN : MEOW_SWIPE_UP;
push_ev(MEOW_EV_SWIPE, dir, ady > adx ? ady : adx);
int dist = ady > adx ? ady : adx;
/* 批②:竖向主导且上移(dir==UP)时,判「底部热区上滑 → 回桌面」。仅当起点在底部
* 热区(最后 HOME_EDGE_H px)且**不在左缘**(down_x>=EDGE_W,与左缘返回互斥)且
* 上移 ≥HOME_SWIPE 且竖向主导(adx<-dy)才命中;否则照常 SWIPE_UP(中部上滑/滚动交
* 由 App)。命中改入队 MEOW_EV_HOME(区别于普通上滑,供 run 循环按兜底回桌面)。 */
if (dir == MEOW_SWIPE_UP &&
meow_ui_input_is_home_swipe(t_dx, t_dy, mdx, mdy, meow_ui_height()))
push_ev(MEOW_EV_HOME, MEOW_SWIPE_UP, dist);
else
push_ev(MEOW_EV_SWIPE, dir, dist);
}
} else {
push_ev(MEOW_EV_TAP, t_cx, t_cy);
@@ -746,6 +765,22 @@ void meow_ui_run(const meow_ui_events *ev)
} else if (e.a == MEOW_SWIPE_LEFT && sb_active) {
meow_ui_request_exit();
}
} else if (e.type == MEOW_EV_HOME) {
/* 系统级「底部热区上滑回桌面」(用户 2026-09-04 批②补全,任意 App 内生效):
* 屏底边缘热区+竖向主导上滑已由 meow_ui_input_is_home_swipe 过滤(与 App 中部
* 滚动、底部横向滑、左缘返回互斥),故作为**系统保留手势**优先于 App on_swipe /
* view 树——否则现有 App(ncm/hello/files/reader 均注册 on_swipe)会接管导致
* App 内上滑不回桌面。命中调 meow_ui_nav_request_exit()(MSG_NAV EXIT → launcher
* 批① meow_bg_add 保活,进程保留转后台、SDK present 自抑);**不**调
* meow_ui_request_exit()——那会断送保活(进程 exit(0))。launcher 据 app 声明
* (background_keepalive/background_audio+媒体活跃)决定无限期/宽限。 */
if (in_startup) continue; /* 启动窗口误触残留:丢弃 */
meow_ui_swipe_hint(-1);
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
/* IME 打开时软键盘覆盖底部:键盘拥有底部区,把事件交给 IME 且**绝不**回桌面
* (防输入时误退)。 */
if (meow_ime_active()) { (void)meow_ime_handle_event(&e); continue; }
meow_ui_nav_request_exit();
} else if (e.type == MEOW_EV_NOTIF_PULL) {
if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */
/* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发
+12 -6
View File
@@ -131,12 +131,6 @@ void meow_gfx_mark_full(void);
* 菜单内容盖到 App 内容上)——叠加前用本函数撤销整帧脏,再按需标局部脏(agent-146)。 */
void meow_gfx_dirty_reset(void);
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
unsigned char *meow_ui_back_buf(void);
/* back_buf 的每行字节数(pitch)与位深(16/32)。M2.71-bug3:模态/IME 快照恢复用。 */
int meow_ui_back_buf_pitch(void);
int meow_ui_back_buf_bpp(void);
/* ---- 模态背景快照/恢复(meow_ui_dialog.c 提供;dialog/sheet/select 共用)----
* open 前调用 capture 把整帧背景拍下(modal_capture),close 后调用 restore 回填 + 整帧上屏
* (modal_restore)→ scrim 每次只叠一层、关闭即还原无残影(M2.71-bug3)。共享同一份快照缓冲,
@@ -144,6 +138,12 @@ int meow_ui_back_buf_bpp(void);
void meow_ui_modal_capture(void);
void meow_ui_modal_restore(void);
/* 返回离屏 back_buf 指针(launcher 系统组件与 SDK 共用同一 back_buf 用)。 */
unsigned char *meow_ui_back_buf(void);
/* back_buf 的每行字节数(pitch)与位深(16/32)。M2.71-bug3:模态/IME 快照恢复用。 */
int meow_ui_back_buf_pitch(void);
int meow_ui_back_buf_bpp(void);
/* 实心圆角矩形(控件按钮底色用;公共 §3 只有空心 draw_rect_round)。[D3] */
void meow_gfx_fill_round(int x, int y, int w, int h, int r, uint32_t color);
/* 圆角矩形半透明填充(M2.71-bug4):禁用变暗等状态层用,圆角外不叠色(防灰方块)。 */
@@ -163,6 +163,12 @@ int meow_gfx_is_device(void);
* (仅 App 进程会自抑;宿主单测断言用,设备上无调用方)。 */
unsigned long long meow_gfx_overlay_suppressed_count(void);
/* §批①多 App 后台常驻:本进程是否应自抑上屏+排空输入(present/input 层共用判定)。
* /tmp/meow_notif_active 内容=前台 app_id:内容==本进程 app_id → 前台不抑制(返回 0);
* 内容为空或其它 app_id → 菜单前台/叠加层覆盖/后台常驻 → 自动抑制(返回 1)。
* 系统 UI(launcher)恒 0;200ms 节流缓存。meow_ui_gfx.c 实现,input 层复用。 */
int meow_ui_overlay_suppressed_active(void);
/* 中文/日文字库(mfont)是否已加载(状态栏「蓝牙」CJK 与回退 "BT" 二选一)。 */
int meow_gfx_mfont_ready(void);
+2 -2
View File
@@ -146,9 +146,9 @@ void meow_log_install_exit_handler(void)
/* M2.87:SIGUSR1 无害护栏(防线冗余)。watchdog 对「左缘起点+右滑」发 SIGUSR1 当返回事件;
* SDK 框架 App(framework=1)自己经 on_swipe 处理左缘返回(meow_ui_input.c),不应被 SIGUSR1
* 终止。但看门狗对 framework 判定有误/竞态/测试注入等**任何**路径漏进 SIGUSR1,默认动作都会
* 杀死 App(某 App signal=16 崩溃根因之一)。本节安装一个不做任何致命动作的处理器:记日志+忽略
* 杀死 App(ncm signal=16 崩溃根因之一)。本节安装一个不做任何致命动作的处理器:记日志+忽略
* ——框架 App 的返回/退出语义由 SDK on_swipe 统一决策,SIGUSR1 仅作「绝不杀 App」的保活护栏,
* 不重复触发返回(避免与 on_swipe 双重返回)。App 若自装 SIGUSR1 处理器(hello 等框架 App 均未装,
* 不重复触发返回(避免与 on_swipe 双重返回)。App 若自装 SIGUSR1 处理器(hello/ncm 均未装,
* 裸沉浸 App 自绘处理)会覆盖本处理器;`--sim-swipe` 测试路径也随之被吞。 */
static void sigusr1_guard(int sig)
{
+1 -1
View File
@@ -214,7 +214,7 @@ void meow_ui_media_stop(void)
/* 物理媒体键 keycode → MSG_PLAYCTL cmd(1=播放 2=暂停 3=上一首 4=下一首 5=seek 6=播放/暂停切换)。
* 播放/暂停(164)恒发 cmd=6 toggle——SDK 本地不再按「最近上报 state」判 play/pause:
* 走自有 IPC 线程的媒体源,不经 meow_ui_media_meta 上报,
* 走自有 IPC 线程的媒体源(如 ncm 的 ipc_media.c)不经 meow_ui_media_meta 上报,
* 本地 state 恒 0 → 恒发「播放」,播放中按键无效(真机 bug)。toggle 由媒体源按自身
* 当前态翻转(权威在源)。上一曲 165/19→3,下一曲 163→4。非媒体键返回 -1。 */
static int media_key_cmd(int code)
+195 -66
View File
@@ -116,6 +116,7 @@ struct meow_widget {
int show_icon; /* 1=画左图标/缩略图(默认 1) */
int show_right;/* 1=画右尾件(默认 1) */
char empty_text[64]; /* 空态占位文案(首字符 0=不画) */
int container; /* MD3 容器模式(默认 renderer):1=行=surfaceContainer 圆角卡+空隙 (P0) */
/* §5.5 左滑删除(列表行横向手势:滑动偏移/删除动画/回弹;calloc 归零,安全)。
* 手势由 App 在 on_touch_move 喂(swipe_begin/move/end);横向小位移判 swipe、纵向滚动优先;
@@ -141,6 +142,12 @@ static uint32_t widget_fg(const meow_widget *w, const meow_theme *th, uint32_t d
{ (void)th; return (w->opts_used && w->opts.fg) ? w->opts.fg : def; }
static int widget_radius(const meow_widget *w, int def)
{ return (w->opts_used && w->opts.radius >= 0) ? w->opts.radius : def; }
/* MD3 全形按钮圆角 = 胶囊(全高一半);opts.radius 显式优先(P1 shape 对齐,替代硬编码 10)。 */
static int widget_btn_radius(const meow_widget *w)
{
if (w->opts_used && w->opts.radius >= 0) return w->opts.radius;
return MEOW_SHAPE_FULL(w->h);
}
static int widget_pady(const meow_widget *w)
{ return (w->opts_used && w->opts.pad_y > 0) ? w->opts.pad_y : 0; }
@@ -400,6 +407,10 @@ meow_widget *meow_list_create(int x, int y, int w, int h, int count,
t->owner = meow_ui_activity_active();
t->state = MEOW_WS_ENABLED;
t->count = count; t->src = src; t->ctx = ctx; t->sel = 0;
/* M6.37 P1:SDK 默认列表(meow_list_create / meow_lazycolumn_create / meow_lazyrow_create,皆 W_LIST)
* 走 MD3 容器模式——每项=surfaceContainer 圆角卡 + 项间空隙 + 去分隔线 + 卡内 tonal 选中/按压,
* 与 meow_listview 容器同范式(全局统一)。需扁平的宿主可显式 meow_list_set_container(w,0) 回退。 */
t->container = 1;
meow_draw_widget_register(t);
return t;
}
@@ -1007,6 +1018,17 @@ int meow_list_tap_at(meow_widget *w, int x, int y)
return idx;
}
/* MD3 容器模式(W_LIST 共用,§5.5/§5.8.6):0=扁平满宽项+分隔线(回退);1=每项外套 surfaceContainer
* 圆角卡+项间空隙(去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal)。
* 覆盖 meow_list_create / meow_lazycolumn_create / meow_lazyrow_create。返回 0=成功;无效=负值。 */
int meow_list_set_container(meow_widget *w, int on)
{
if (!w || w->type != W_LIST) return -1;
w->container = on ? 1 : 0;
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
return 0;
}
/* ============================================================ */
/* §5.5 结构化列表(W_LISTVIEW):行=图标+标题+副标题+右侧操作+选中
* ------------------------------------------------------------
@@ -1017,6 +1039,11 @@ int meow_list_tap_at(meow_widget *w, int x, int y)
#define MEOW_LISTV_ICON 24 /* 图标盒尺寸(px) */
#define MEOW_LISTV_PAD 16 /* 行内左右 padding(px) */
#define MEOW_LISTV_SUB_GAP 2 /* 标题与副标题间隙(px) */
/* MD3 容器模式(P0):行=surfaceContainer 圆角卡 + 行间空隙(去分隔线);卡图元与主界面/
* ncm 歌单同手法(main_menu.c:639 / playlist.c)。卡在行高内上下对称内缩(行中心=卡中心)。 */
#define MEOW_LISTV_CARD_MARGIN 8 /* 卡横向外边距(px) */
#define MEOW_LISTV_CARD_GAP 8 /* 卡纵向间隙(px) */
#define MEOW_LISTV_CARD_RADIUS 12 /* 卡圆角(px,12dp) */
/* meow_ui_img_scaled 弱引用:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支用它绘制图片图标。
* 弱引用让「只用内置符号图标、不解码图片」的 App 不被牵连进 meow_ui_img.o/stb_image.o
@@ -1179,6 +1206,9 @@ meow_widget *meow_listview_create(int x, int y, int w, int h, int count,
t->sel = -1; t->item_h = 56; t->scroll = 0; t->swipe_idx = -1;
t->sel_mode = (mode >= MEOW_LIST_SEL_NONE && mode <= MEOW_LIST_SEL_MULTI) ? mode : MEOW_LIST_SEL_NONE;
t->lines = 2; t->show_icon = 1; t->show_right = 1; /* 默认双行富 item + 图标 + 右尾件(src2 布局不变) */
t->container = 1; /* M6.37 P1:SDK 默认列表走 MD3 容器模式——行=surfaceContainer 圆角卡 + 空隙 + 去分隔线 + 卡内 tonal 选中,
* 让 hello 示例 / files 及一切用 SDK 默认 renderer 的 App 开箱即 MD3(用户 2026-09-04 反馈默认仍扁平)。
* 需扁平的宿主可显式 meow_listview_set_container(lv,0) 回退(test_meow_ui_widgets 即用它锁扁平基线)。 */
if (t->sel_mode == MEOW_LIST_SEL_MULTI && t->count > 0) {
t->sel_bits_n = (t->count + 31) / 32;
t->sel_bits = (uint32_t *)calloc((size_t)t->sel_bits_n, sizeof(uint32_t));
@@ -1395,6 +1425,14 @@ int meow_listview_set_item_draw(meow_widget *w, meow_listview_draw_fn draw,
return 0;
}
int meow_listview_set_container(meow_widget *w, int on)
{
if (!w || w->type != W_LISTVIEW) return -1;
w->container = on ? 1 : 0;
meow_gfx_mark_dirty(w->x, w->y, w->w, w->h);
return 0;
}
int meow_listview_set_empty_text(meow_widget *w, const char *text)
{
if (!w || w->type != W_LISTVIEW) return -1;
@@ -1631,7 +1669,7 @@ void meow_widget_draw(meow_widget *w)
uint32_t bg;
if (fill) bg = (hold) ? state_tint(base, th, 24) : (press ? state_tint(base, th, 12) : base);
else bg = 0;
int r = widget_radius(w, 10);
int r = widget_btn_radius(w);
if (fill) meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, bg);
else if (press) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->primary, 12); /* transparent ripple */
/* 内容区受 pad 垂直内缩(水平居中文本,pad_x 不影响中心) */
@@ -1693,17 +1731,25 @@ void meow_widget_draw(meow_widget *w)
break;
}
case W_SWITCH: {
uint32_t bg = w->on ? th->primary : th->onSurfaceVariant;
/* MD3(P3):on-track=primary;off-track=surfaceContainerHighest(原 onSurfaceVariant 过暗);
* on-thumb=onPrimary;off-thumb=surface + outline(原恒白)。 */
uint32_t bg = w->on ? th->primary : th->surfaceContainerHighest;
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);
uint32_t knob = w->on ? th->onPrimary : th->surface; /* off-thumb 表面色 */
meow_gfx_fill_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, knob);
if (!w->on) meow_ui_draw_rect_round(kx, w->y + 3, knob_r * 2, knob_r * 2, knob_r, th->outline);
if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->h / 2, th->onSurface, 90);
break;
}
case W_LIST: {
/* §5.8.6 列表容器(LazyColumn/LazyRow):只画可见项(虚拟化)+ 滚动偏移。
* 纵向=行高 item_h(默认 44,兼容既有 meow_list_create);横向=LazyRow(列宽=item_h)。 */
* 纵向=行高 item_h(默认 44,兼容既有 meow_list_create);横向=LazyRow(列宽=item_h)。
* MD3 容器模式(w->container=1,默认):每项外套 surfaceContainer 圆角卡(12dp)+ 项间空隙
* (去分隔线),选中=primaryContainer/按压=primary α26(卡内 tonal),与 meow_listview 容器同范式;
* 卡在项内交叉轴内缩 MEOW_LISTV_CARD_MARGIN、滚动轴对称内缩 MEOW_LISTV_CARD_GAP(行中心=卡中心)。
* 非容器(meow_list_set_container(0) 回退):满幅扁平行 + primary 选中 + onSurfaceVariant 分隔线。 */
int pad = 16;
int item = list_item(w);
int vp = w->horizontal ? w->w : w->h;
@@ -1720,36 +1766,70 @@ void meow_widget_draw(meow_widget *w)
* 画到视口外 → 标脏并上屏 → 标题栏/底部残留脏区,P3)。 */
int vx0 = w->x, vy0 = w->y, vx1 = w->x + w->w, vy1 = w->y + w->h;
int thh = meow_ui_text_height(th->font_body);
int cm = w->container ? MEOW_LISTV_CARD_MARGIN : 0; /* 卡交叉轴内缩(纵向=水平→横向=垂直) */
int cgap = w->container ? MEOW_LISTV_CARD_GAP : 0; /* 卡滚动轴对称内缩(去分隔线、留空隙) */
for (int i = first; i < last; i++) {
char label[64] = "";
if (!w->src || !w->src(i, label, (int)sizeof label, w->ctx)) continue;
int pos = i * item - off; /* 相对容器起点的可视位置 */
int sel = (i == w->sel);
if (w->horizontal) {
int rx = w->x + pos;
if (rx >= vx1) break; /* 完全在右缘外 */
if (rx + item <= vx0) continue; /* 完全在左缘外(滚出残余) */
int cl = rx > vx0 ? rx : vx0; /* 可见左界 */
int cr = (rx + item) < vx1 ? (rx + item) : vx1; /* 可见右界 */
if (i == w->sel) meow_ui_fill_rect(cl, w->y, cr - cl, w->h, th->primary);
uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface;
int ctop = w->y + cm; /* 卡顶(交叉轴内缩) */
int chh = w->h - 2 * cm; /* 卡高 */
int clft = rx + cgap / 2; /* 卡左(滚动轴对称内缩) */
int cwid = item - cgap; /* 卡宽 */
int press = meow_input_touch_down() &&
meow_input_touch_x() >= rx && meow_input_touch_x() < rx + item &&
meow_input_touch_y() >= w->y && meow_input_touch_y() < w->y + w->h;
if (w->container) {
meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
if (sel) meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
} else {
if (sel) meow_ui_fill_rect(cl, w->y, cr - cl, w->h, th->primary);
}
uint32_t col = sel ? (w->container ? th->onPrimaryContainer : 0xFFFFFF) : th->onSurface;
int ty = w->y + (w->h - thh) / 2;
if (rx >= vx0 && rx + item <= vx1) /* 仅整项可见才画文本(防泄漏) */
meow_ui_text_center(rx + item / 2, ty, label, th->font_body, col);
int sxp = rx + item - 1; /* 分隔线:仅当落在可见区 */
if (sxp >= cl && sxp < cr) meow_ui_fill_rect(sxp, w->y, 1, w->h, th->onSurfaceVariant);
if (!w->container) { /* 非容器才画分隔线(容器模式去冗余) */
int sxp = rx + item - 1; /* 分隔线:仅当落在可见区 */
if (sxp >= cl && sxp < cr) meow_ui_fill_rect(sxp, w->y, 1, w->h, th->onSurfaceVariant);
}
} else {
int ry = w->y + pos;
if (ry >= vy1) break; /* 完全在底缘外 */
if (ry + item <= vy0) continue; /* 完全在顶缘外(滚出残余) */
int cl = ry > vy0 ? ry : vy0; /* 可见顶界 */
int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */
if (i == w->sel) meow_ui_fill_rect(w->x, cl, w->w, cr - cl, th->primary);
uint32_t col = (i == w->sel) ? 0xFFFFFF : th->onSurface;
int clft = w->x + cm; /* 卡左(交叉轴内缩) */
int cwid = w->w - 2 * cm; /* 卡宽 */
int ctop = ry + cgap / 2; /* 卡顶(滚动轴对称内缩) */
int chh = item - cgap; /* 卡高 */
int press = meow_input_touch_down() &&
meow_input_touch_x() >= w->x && meow_input_touch_x() < w->x + w->w &&
meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item;
if (w->container) {
meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
if (sel) meow_gfx_fill_round(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(clft, ctop, cwid, chh, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
} else {
if (sel) meow_ui_fill_rect(w->x, cl, w->w, cr - cl, th->primary);
}
uint32_t col = sel ? (w->container ? th->onPrimaryContainer : 0xFFFFFF) : th->onSurface;
int txx = (w->container ? clft : w->x) + pad; /* 卡内文本左缘 */
int ty = ry + (item - thh) / 2;
if (ty >= vy0 && ty + thh <= vy1) /* 文本不越界才画(防标题/底部泄漏) */
meow_ui_text(w->x + pad, ty, label, th->font_body, col);
int sy = ry + item - 1; /* 分隔线:仅当落在可见区 */
if (sy >= cl && sy < cr) meow_ui_fill_rect(w->x, sy, w->w, 1, th->onSurfaceVariant);
meow_ui_text(txx, ty, label, th->font_body, col);
if (!w->container) { /* 非容器才画分隔线(容器模式去冗余) */
int sy = ry + item - 1; /* 分隔线:仅当落在可见区 */
if (sy >= cl && sy < cr) meow_ui_fill_rect(w->x, sy, w->w, 1, th->onSurfaceVariant);
}
}
}
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
@@ -1777,11 +1857,16 @@ void meow_widget_draw(meow_widget *w)
int vx0 = w->x, vy0 = w->y, vy1 = w->y + w->h;
int thh = meow_ui_text_height(th->font_body);
int pad = MEOW_LISTV_PAD, isz = MEOW_LISTV_ICON;
int text_x = vx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */
int op_start = (w->show_right) ? (vx0 + (w->w * 2 / 3)) /* 行右 1/3 = 操作区 */
: (vx0 + w->w - pad);
/* 容器模式(P0):行=surfaceContainer 圆角卡,卡横向内缩 cm;内容左缘/操作区/可用宽
* 都在卡内计算(非容器 cm=0 → 与既有行为逐字一致)。 */
int cm = w->container ? MEOW_LISTV_CARD_MARGIN : 0;
int cw = w->w - 2 * cm; /* 内容/卡宽 */
int cx0 = vx0 + cm; /* 内容左缘(卡内) */
int text_x = cx0 + pad + ((w->show_icon) ? (isz + pad) : 0); /* 无图标则无图标间隙 */
int op_start = (w->show_right) ? (cx0 + (cw * 2 / 3)) /* 行右 1/3 = 操作区 */
: (cx0 + cw - pad);
int text_maxw = (w->show_right) ? (op_start - pad - text_x) /* 标题/副标题可用宽(不压操作区) */
: ((vx0 + w->w - pad) - text_x);
: ((cx0 + cw - pad) - text_x);
if (text_maxw < 8) text_maxw = 8;
/* 空态占位(§5.5):count==0 且设了 empty_text → 视口内居中画文案。 */
@@ -1841,26 +1926,36 @@ void meow_widget_draw(meow_widget *w)
meow_str("common.delete"), th->font_body, th->onError);
shift = w->swipe_dx;
}
int rlx = vx0 + shift; /* 行内容左移后的 x */
int rlx = (w->container ? cx0 : vx0) + shift; /* 行内容/卡左缘(随行左移) */
int rtx = text_x + shift; /* 文本 x(随行左移) */
int rop = op_start + shift; /* 操作区分界 x */
int card_y = ry + MEOW_LISTV_CARD_GAP / 2; /* 容器模式:卡顶(行内对称内缩) */
int card_h = item - MEOW_LISTV_CARD_GAP;
if (it->section) { /* 段头占位行(非粘性):surfaceContainerHigh 底 + primary 段名 */
meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->surfaceContainerHigh);
if (it->section) { /* 段头占位行(非粘性):容器=surface 上的 primary 段标签;非容器=surfaceContainerHigh 底 */
if (!w->container) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->surfaceContainerHigh);
int sty = ry + (item - thh) / 2;
if (it->title && sty >= vy0 && sty + thh <= vy1)
listv_text_trunc(rtx, sty, it->title, text_maxw, th->font_body, th->primary);
int sy = ry + item - 1;
if (sy >= cl && sy < cr)
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
if (!it->enabled)
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
if (!w->container) {
int sy = ry + item - 1;
if (sy >= cl && sy < cr)
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
if (!it->enabled)
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
}
continue;
}
/* 选中/按压背景(primaryContainer / 半透明主题色按压层) */
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26);
/* 选中/按压背景(容器=卡内 tonal;非容器=满幅实心,原行为) */
if (w->container) {
meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
if (sel) meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
} else {
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26);
}
uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface;
uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
@@ -1890,19 +1985,25 @@ void meow_widget_draw(meow_widget *w)
/* 右尾件(show_right=1:箭头/开关/复选/⋯) */
if (w->show_right && it->right != MEOW_LIST_RIGHT_NONE) {
int ax = rlx + w->w - pad - 24; /* 右缘动作槽(约 24px) */
int ax = (cx0 + cw - pad - 24) + shift; /* 右缘动作槽(约 24px;容器=卡内右缘) */
if (ax >= rop)
listv_draw_right(th, ax, ry, item, it->right, it->on, subcol);
}
/* 分隔线(仅落在可见带内才画) */
int sy = ry + item - 1;
if (sy >= cl && sy < cr)
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
/* 分隔线(仅非容器画;容器模式去冗余分隔线,行间用空隙替代) */
if (!w->container) {
int sy = ry + item - 1;
if (sy >= cl && sy < cr)
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
}
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */
if (!it->enabled)
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗;容器=卡内淡) */
if (!it->enabled) {
if (w->container)
meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->onSurface, 90);
else
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
}
}
}
} else {
@@ -1918,10 +2019,13 @@ void meow_widget_draw(meow_widget *w)
int cl = ry > vy0 ? ry : vy0; /* 可见顶界(裁剪用) */
int cr = (ry + item) < vy1 ? (ry + item) : vy1; /* 可见底界 */
int sel = (i == w->sel);
int press = meow_input_touch_down() &&
meow_input_touch_x() >= vx0 && meow_input_touch_x() < vx0 + w->w &&
meow_input_touch_y() >= ry && meow_input_touch_y() < ry + item;
uint32_t textcol = sel ? th->onPrimaryContainer : th->onSurface;
uint32_t subcol = sel ? th->onPrimaryContainer : th->onSurfaceVariant;
/* 左滑删除(M7.3):本行被滑动 → 右露删除区 + 行 x 左移。非滑动行 shift=0 → 输出不变。 */
int shift = 0, rlx = vx0, rtx = text_x, rop = op_start;
int shift = 0, rlx = (w->container ? cx0 : vx0), rtx = text_x, rop = op_start;
if (w->swipe_active && w->swipe_idx == i && w->swipe_dx < 0) {
meow_ui_fill_rect(vx0 + w->w + w->swipe_dx, ry, -w->swipe_dx, item, th->error);
int thh2 = meow_ui_text_height(th->font_body);
@@ -1929,9 +2033,19 @@ void meow_widget_draw(meow_widget *w)
if (dly >= vy0 && dly + thh2 <= vy1)
meow_ui_text_center(vx0 + w->w + w->swipe_dx + (-w->swipe_dx) / 2, dly,
meow_str("common.delete"), th->font_body, th->onError);
shift = w->swipe_dx; rlx = vx0 + shift; rtx = text_x + shift; rop = op_start + shift;
shift = w->swipe_dx; rlx = (w->container ? cx0 : vx0) + shift;
rtx = text_x + shift; rop = op_start + shift;
}
int card_y = ry + MEOW_LISTV_CARD_GAP / 2;
int card_h = item - MEOW_LISTV_CARD_GAP;
if (w->container) {
meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->surfaceContainer);
if (sel) meow_gfx_fill_round(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primaryContainer);
else if (press) meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->primary, 26);
} else {
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
else if (press) meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->primary, 26);
}
if (sel) meow_ui_fill_rect(rlx, cl, w->w, cr - cl, th->primaryContainer);
/* 图标(左) */
uint32_t iccol = sel ? th->onPrimaryContainer
@@ -1958,19 +2072,25 @@ void meow_widget_draw(meow_widget *w)
/* 右侧操作区 */
if (it.right != MEOW_LIST_RIGHT_NONE) {
int ax = rlx + w->w - pad - 24; /* 右缘动作槽(约 24px) */
int ax = (cx0 + cw - pad - 24) + shift; /* 右缘动作槽(约 24px;容器=卡内右缘) */
if (ax >= rop)
listv_draw_right(th, ax, ry, item, it.right, it.on, subcol);
}
/* 分隔线(仅落在可见带内才画) */
int sy = ry + item - 1;
if (sy >= cl && sy < cr)
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
/* 分隔线(仅非容器画;容器模式去冗余分隔线,行间用空隙替代) */
if (!w->container) {
int sy = ry + item - 1;
if (sy >= cl && sy < cr)
meow_ui_fill_rect(rlx, sy, w->w, 1, th->outlineVariant);
}
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗) */
if (!it.enabled)
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
/* 禁用行整体变淡(§5.5/§5.8.3 enabled=0 → 变暗;容器=卡内淡) */
if (!it.enabled) {
if (w->container)
meow_gfx_fill_round_alpha(rlx, card_y, cw, card_h, MEOW_LISTV_CARD_RADIUS, th->onSurface, 90);
else
meow_ui_fill_rect_alpha(rlx, cl, w->w, cr - cl, th->onSurface, 90);
}
}
}
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
@@ -1988,7 +2108,7 @@ void meow_widget_draw(meow_widget *w)
break;
case W_TEXTFIELD: {
/* MD3 输入框:圆角外框(surfaceContainerLowest 底 + outline 描边;focused=primary 2px)+ 提示/文本 + 光标。 */
int r = widget_radius(w, 8);
int r = widget_radius(w, MEOW_SHAPE_SMALL);
int focused = (w->state & MEOW_WS_FOCUSED) != 0;
meow_gfx_fill_round(w->x, w->y, w->w, w->h, r, th->surfaceContainerLowest);
uint32_t bcol = focused ? th->primary : th->outline;
@@ -2045,55 +2165,63 @@ void meow_widget_draw(meow_widget *w)
break;
}
case W_CHECKBOX: {
/* MD3(P3):视觉字 24dp 于控件矩形内居中(触摸目标不变);修正原 34 视觉过大。 */
int sel = (w->state & MEOW_WS_SELECTED) != 0;
int sz = w->w;
int r = widget_radius(w, 4);
int sz = 24;
if (w->w < sz) sz = w->w; /* 控件小于视觉尺寸时跟随 */
int bx = w->x + (w->w - sz) / 2, by = w->y + (w->h - sz) / 2;
int r = widget_radius(w, MEOW_SHAPE_EXTRASMALL);
if (sel) {
meow_gfx_fill_round(w->x, w->y, sz, sz, r, th->primary);
int ax = w->x + 9, ay1 = w->y + 17, ay2 = w->y + 23, bx = w->x + 15, cx = w->x + 25, cy = w->y + 11;
meow_gfx_line(ax, ay1, bx, ay2, 3, th->onPrimary);
meow_gfx_line(bx, ay2, cx, cy, 3, th->onPrimary);
meow_gfx_fill_round(bx, by, sz, sz, r, th->primary);
int ax = bx + 7, ay1 = by + 13, ay2 = by + 17, b2 = bx + 11, cx = bx + 18, cy = by + 7;
meow_gfx_line(ax, ay1, b2, ay2, 2, th->onPrimary);
meow_gfx_line(b2, ay2, cx, cy, 2, th->onPrimary);
} else {
meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->outline);
meow_ui_draw_rect_round(bx, by, sz, sz, r, th->outline);
}
if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(w->x, w->y, sz, sz, r, th->primary);
if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, sz, sz, r, th->onSurface, 90);
if (w->state & MEOW_WS_FOCUSED) meow_ui_draw_rect_round(bx, by, sz, sz, r, th->primary);
if (disabled) meow_gfx_fill_round_alpha(bx, by, sz, sz, r, th->onSurface, 90);
break;
}
case W_RADIO: {
/* MD3(P3):视觉 20dp(环半径 10)于控件内居中;原环半径 12 略大。 */
int sel = (w->state & MEOW_WS_SELECTED) != 0;
int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2;
meow_gfx_ring_arc(ccx, ccy, 12, 2, 0, 360, sel ? th->primary : th->outline);
if (sel) meow_gfx_fill_circle(ccx, ccy, 6, th->primary);
meow_gfx_ring_arc(ccx, ccy, 10, 2, 0, 360, sel ? th->primary : th->outline);
if (sel) meow_gfx_fill_circle(ccx, ccy, 5, th->primary);
if (disabled) meow_gfx_fill_round_alpha(w->x, w->y, w->w, w->h, w->w / 2, th->onSurface, 90);
break;
}
case W_PROGRESS: {
/* 批5b:轨道/填充可由 opts 覆盖(opts.bg=轨道、opts.fg=填充);opts 未设则回主题默认
* (surfaceVariant/primary)——兼容既有调用(如 hello demo、现在播放页取色后回退默认)。 */
uint32_t track = widget_bg(w, th, th->surfaceVariant);
uint32_t fill = widget_fg(w, th, th->primary);
if (w->variant == 1) { /* 圆形(几何 = 整圆像素外接框;由此解回圆心/半径) */
int thk = MEOW_PROGRESS_ARC_THK;
int m = MEOW_PROGRESS_ARC_MARGIN;
int ccx = w->x + w->w / 2, ccy = w->y + w->h / 2;
int rr = w->w / 2 - m; /* 与 create 一致:回到调用方传入的半径 r */
if (rr <= 0) break;
meow_gfx_ring_arc(ccx, ccy, rr, thk, 0, 360, th->surfaceVariant);
meow_gfx_ring_arc(ccx, ccy, rr, thk, 0, 360, track);
if (w->value < 0) { /* 不定:按时间推进一段弧(App 在 on_tick 里重绘驱动) */
int start = (int)((meow_ui_time_ms() / 30) % 360);
meow_gfx_ring_arc(ccx, ccy, rr, thk, start - 90, 90, th->primary);
meow_gfx_ring_arc(ccx, ccy, rr, thk, start - 90, 90, fill);
} else if (w->value > 0) {
int sweep = w->value * 360 / 100;
if (sweep > 0) meow_gfx_ring_arc(ccx, ccy, rr, thk, -90, sweep, th->primary);
if (sweep > 0) meow_gfx_ring_arc(ccx, ccy, rr, thk, -90, sweep, fill);
}
} else { /* 线性 */
int thk = w->h > 0 ? w->h : 4;
int yy = w->y + w->h / 2 - thk / 2;
meow_ui_fill_rect(w->x, yy, w->w, thk, th->surfaceVariant);
meow_ui_fill_rect(w->x, yy, w->w, thk, track);
if (w->value < 0) { /* 不定:移动块 */
int fw = w->w / 3;
int pos = (int)((meow_ui_time_ms() / 12) % (w->w + fw)) - fw;
meow_ui_fill_rect(w->x + pos, yy, fw, thk, th->primary);
meow_ui_fill_rect(w->x + pos, yy, fw, thk, fill);
} else if (w->value > 0) {
int fw = w->value * w->w / 100;
if (fw > 0) meow_ui_fill_rect(w->x, yy, fw, thk, th->primary);
if (fw > 0) meow_ui_fill_rect(w->x, yy, fw, thk, fill);
}
}
if (disabled) meow_ui_fill_rect_alpha(w->x, w->y, w->w, w->h, th->onSurface, 90);
@@ -2150,7 +2278,8 @@ void meow_widget_draw(meow_widget *w)
for (int i = 0; i < w->nitems; i++) {
int ix = w->x + i * iw;
int sel = (i == w->selected);
if (sel) meow_gfx_fill_round(ix + iw / 2 - 24, w->y + 8, 48, 22, 11, th->secondaryContainer);
if (sel) /* MD3 active pill 64×32(原 48×22 偏小);垂直居中于栏 */
meow_gfx_fill_round(ix + iw / 2 - 32, w->y + (w->h - 32) / 2, 64, 32, 16, th->secondaryContainer);
uint32_t col = sel ? th->onSecondaryContainer : th->onSurfaceVariant;
meow_ui_text_center(ix + iw / 2, ay, w->items[i], th->font_body, col);
}
+189
View File
@@ -0,0 +1,189 @@
/* test_meow_cachegov.c — 宿主测试:SDK meow_cachegov(A:DONTNEED L0/L1 + refault 回退)。
*
* 背景(OOM_PAGECACHE_GOVERNANCE §2/§4,③混合):A 的 DONTNEED/read_pos 热路径随 SDK 库;
* 本测试用宿主桩:
* - 真实 posix_fadvise(文件数据完整性校验:drop 后文件内容不变、drop_finished 可再读);
* - meow_log 桩捕获 "[cachegov] L1 drop off=.. len=.." 断言丢弃范围([0, read_pos-keep_hot));
* - meow_cachegov_test_set_time_ms() 注入时钟驱动 refault 滚窗回退(放大 keep_hot→降L0→关A);
* - MEOW_CACHEGOV_C_FILE 指向临时文件写入 fake C 状态,断言 download_paused/prefetch_paused。
*
* 编译(patch_v2/sdk 目录下):
* gcc -Os -Wall -Wextra -I. -Iinclude -I../meow_launcherd -c meow_cachegov.c -o /tmp/cg.o
* gcc -Os -Wall -Wextra -I. -Iinclude -I../meow_launcherd test_meow_cachegov.c /tmp/cg.o -lpthread -o test_meow_cachegov
* 运行:./test_meow_cachegov (exit 0=PASS / 1=FAIL)
*/
#define _GNU_SOURCE
#include "meow_cachegov.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
/* ---- meow_log 桩:捕获最近一条 [cachegov] 行,供断言 ---- */
static char g_last_log[512];
void meow_log(int lvl, const char *fmt, ...)
{
(void)lvl;
va_list ap; va_start(ap, fmt);
vsnprintf(g_last_log, sizeof g_last_log, fmt, ap);
va_end(ap);
printf(" [cachegov.log] %s\n", g_last_log);
}
static int log_has(const char *needle) { return strstr(g_last_log, needle) != NULL; }
static int g_pass = 0, g_fail = 0;
static void check(const char *name, int cond)
{
if (cond) { g_pass++; printf("PASS %s\n", name); }
else { g_fail++; printf("FAIL %s\n", name); }
}
/* ---- read_pos 桩:由测试驱动当前读指针 ---- */
static uint64_t g_pos;
static uint64_t fake_read_pos(void *ctx) { (void)ctx; return g_pos; }
/* 建一个 size 字节的临时文件并填充确定性内容。 */
static int make_file(const char *path, size_t size)
{
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return -1;
for (size_t off = 0; off < size; off += 4096) {
char tmp[4096];
memset(tmp, (int)(off / 4096) & 0xFF, sizeof tmp);
size_t n = (size - off) < sizeof tmp ? (size - off) : sizeof tmp;
if (write(fd, tmp, n) != (ssize_t)n) { close(fd); return -1; }
}
close(fd);
return 0;
}
/* 校验文件内容仍为确定性模式(drop 后数据完整)。 */
static int verify_file(const char *path, size_t size)
{
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
char tmp[4096];
size_t off = 0;
int ok = 1;
while (off < size) {
size_t n = (size - off) < sizeof tmp ? (size - off) : sizeof tmp;
ssize_t r = read(fd, tmp, n);
if (r != (ssize_t)n) { ok = 0; break; }
char expect[4096];
memset(expect, (int)(off / 4096) & 0xFF, n);
if (memcmp(tmp, expect, n) != 0) { ok = 0; break; }
off += (size_t)n;
}
close(fd);
return ok;
}
int main(void)
{
char path[320], cfile[360];
snprintf(path, sizeof path, "/tmp/meow_cg_%ld.bin", (long)getpid());
snprintf(cfile, sizeof cfile, "/tmp/meow_cg_%ld.state", (long)getpid());
setenv("MEOW_CACHEGOV_C_FILE", cfile, 1);
unlink(path); unlink(cfile);
const size_t size = 1024u * 1024u; /* 1MB 测试文件 */
check("make_file", make_file(path, size) == 0);
/* 1) cfg:默认 + set/get 往返 */
struct meow_cachegov_cfg c = {0};
meow_cachegov_get_cfg(&c);
check("cfg default enabled", c.enabled == 1);
check("cfg default keep_hot=256KB", c.keep_hot_bytes == 256u * 1024u);
struct meow_cachegov_cfg c2 = c;
c2.keep_hot_bytes = 128u * 1024u;
c2.drop_mode = 1;
c2.refault_backoff = 2;
check("set_cfg", meow_cachegov_set_cfg(&c2) == 0);
meow_cachegov_get_cfg(&c);
check("get_cfg roundtrip", c.keep_hot_bytes == 128u * 1024u && c.drop_mode == 1 && c.refault_backoff == 2);
/* 2) L1 drop:read_pos=512KB、keep_hot=128KB → drop [0, 384KB) off=0 len=384KB */
check("register", meow_cachegov_register(path, size, fake_read_pos, NULL) == 0);
g_pos = 512u * 1024u;
meow_cachegov_tick();
check("L1 drop off=0 len=384KB", log_has("L1 drop") && strstr(g_last_log, "off=0 len=393216"));
check("L1 range bounded by read_pos-keep_hot (no L2)",
!strstr(g_last_log, "off=393216") && strstr(g_last_log, "len=393216"));
/* 3) L1 推进:read_pos=800KB → drop [384KB, 800-128KB=672KB) off=384KB len=288KB */
g_pos = 800u * 1024u;
meow_cachegov_tick();
check("L1 drop off=384KB len=288KB", log_has("L1 drop") && strstr(g_last_log, "off=393216") && strstr(g_last_log, "len=294912"));
/* 4) refault 回退档1(放大 keep_hot 翻倍):3 次触发 2 次 → 0→1(每升降一档需间隔 ≥10s */
meow_cachegov_test_set_time_ms(0);
meow_cachegov_note_refault(); meow_cachegov_note_refault();
meow_cachegov_tick();
check("backoff -> 1 (refault*2)", meow_cachegov_backoff_level() == 1);
check("file intact after backoff1", verify_file(path, size));
/* 档1 且 keep_hot=128KB*2=256KB:read_pos=1000KB → drop 上界=744KB(已丢到 672KB) */
g_pos = 1000u * 1024u;
meow_cachegov_tick();
check("backoff1 L1 keep_hot enlarged (no L2)", meow_cachegov_backoff_level() == 1);
/* 5) 再 2 次(间隔 10s)→ 档2(L1 关,只 L0)→ download_paused=1 */
meow_cachegov_test_set_time_ms(11000);
meow_cachegov_note_refault(); meow_cachegov_note_refault();
meow_cachegov_tick();
check("backoff -> 2 (L1 off)", meow_cachegov_backoff_level() == 2);
g_pos = 900u * 1024u;
meow_cachegov_tick(); /* L1 off:应无新的 L1 drop 日志 */
check("level2 no L1 drop (only L0)", !log_has("L1 drop"));
check("download_paused @backoff>=2", meow_cachegov_download_paused() == 1);
/* 6) 再 2 次(间隔 10s)→ 档3(A 全关)→ prefetch_paused=1 + drop_finished 不再丢页 */
meow_cachegov_test_set_time_ms(22000);
meow_cachegov_note_refault(); meow_cachegov_note_refault();
meow_cachegov_tick();
check("backoff -> 3 (A off)", meow_cachegov_backoff_level() == 3);
check("prefetch_paused @backoff>=3", meow_cachegov_prefetch_paused() == 1);
meow_cachegov_test_set_time_ms(23000);
meow_cachegov_drop_finished(path);
check("drop_finished skip @A-off", log_has("drop_finished skip"));
check("file intact after A-off", verify_file(path, size));
/* 7) C 侧降载:write fake C state dl_pause=1;滚窗干净逐级降回 0 后 download_paused 仍 =1 */
{
FILE *f = fopen(cfile, "w");
check("write C state", f != NULL);
if (f) { fputs("cached_kb=35000\ndirty_kb=1000\ndl_pause=1\nprefetch_pause=0\n", f); fclose(f); }
}
meow_cachegov_test_set_time_ms(33000);
meow_cachegov_tick(); /* 3→2 */
meow_cachegov_test_set_time_ms(44000);
meow_cachegov_tick(); /* 2→1 */
meow_cachegov_test_set_time_ms(55000);
meow_cachegov_tick(); /* 1→0 */
check("backoff de-escalated to 0", meow_cachegov_backoff_level() == 0);
check("download_paused via C file", meow_cachegov_download_paused() == 1);
/* 8) L0 drop_finished(A 正常态):整文件 DONTNEED + 数据完整 + 注销条目 */
unlink(cfile);
meow_cachegov_register(path, size, fake_read_pos, NULL);
meow_cachegov_test_set_time_ms(66000);
meow_cachegov_drop_finished(path);
check("drop_finished L0 log", log_has("drop_finished L0"));
check("file intact after L0", verify_file(path, size));
check("unregister removes entry", meow_cachegov_unregister(path) != 0); /* 已注销 → 找不到 */
/* 9) disable 时 drop/tick 均 no-op:数据仍完整 */
c2.enabled = 0; meow_cachegov_set_cfg(&c2);
meow_cachegov_register(path, size, fake_read_pos, NULL);
g_pos = 512u * 1024u;
meow_cachegov_tick();
meow_cachegov_drop_finished(path);
check("file intact when disabled", verify_file(path, size));
unlink(path); unlink(cfile);
printf("----\n%d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+225
View File
@@ -0,0 +1,225 @@
/* test_meow_img_color.c — 批5 封面双色渐变取色宿主验证。
*
* 覆盖矩阵(对齐任务批5):
* C1 纯色 → 主色=该色(桶平均精确);无显著次桶 → 次色=主色缩暗 2/3(渐变互补)
* C2 两色图 → 主/次正确分离(各桶平均精确)
* C3 近黑/近白/透明跳过:近黑+鲜艳 → 主=鲜艳;全近黑/全近白/全透明 → -1(回退主题)
* C4 尺寸缩放/副采样:12×12 与 120×120 同内容 → 主色不变
* C5 meow_img_duocolor 桥接(局部 struct meow_img 直读 rgba)
* C6 meow_img_color_derive:可读 fg/accent_fg、背景渐变、scrim=主色压暗
* C7 set_colors 注入/回退(lyric_view 层):注入 derive.lyr → cur=fg、gray 随 cur;
* NULL 回退 meow_theme 默认
*
* 编译(在 patch_v2/sdk 下;链法同 test_meow_lyric_view + 单链 meow_img_color.c):
* gcc -Wall -Wextra -o test_meow_img_color test_meow_img_color.c test_host_stub.c \
* meow_img_color.c meow_lyric.c meow_lyric_view.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_img_color
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp(如需真实像素断言) */
#include "meow_lyric_view.h"
#include "meow_img_color.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
/* 局部补全 meow_img(与 meow_ui_img.c / meow_img_color.c 布局一致):构造假封面做桥接测试。 */
struct meow_img { int w, h; unsigned char *rgba; };
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
/* 构造 RGBA 缓冲:rows 描述每行占 w 空格,n 个 (start,count,r,g,b,a) 段。 */
static unsigned char *make_rgba(int w, int h)
{
unsigned char *buf = (unsigned char *)calloc((size_t)w * h * 4, 1);
assert(buf);
return buf;
}
static void fill_px(unsigned char *buf, int w, int x, int y, int r, int g, int b, int a)
{
unsigned char *p = buf + ((size_t)y * w + (size_t)x) * 4;
p[0] = (unsigned char)r; p[1] = (unsigned char)g; p[2] = (unsigned char)b; p[3] = (unsigned char)a;
}
/* 填充一块实心色(x0,y0,w0,h0)。 */
static void fill_rect_px(unsigned char *buf, int W, int x0, int y0, int w0, int h0,
int r, int g, int b, int a)
{
for (int y = y0; y < y0 + h0; y++)
for (int x = x0; x < x0 + w0; x++)
fill_px(buf, W, x, y, r, g, b, a);
}
static void c1_solid(void)
{
printf("C1 纯色 → 主色精确,次色=主色缩暗 2/3\n");
int W = 12, H = 12;
unsigned char *buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, H, 0x30, 0x60, 0xA0, 0xFF);
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
CHECK(rc == 0, "纯色取色成功");
CHECK(m == 0x3060A0, "主色==0x3060A0(桶中心精确)");
CHECK(s == 0x20406A, "次色==0x20406A(主色缩暗 2/3 渐变互补)");
free(buf);
}
static void c2_two_colors(void)
{
printf("C2 两色图 → 主/次分离\n");
int W = 16, H = 16;
unsigned char *buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, 8, 0xE0, 0x30, 0x40, 0xFF); /* 上半:红 */
fill_rect_px(buf, W, 0, 8, W, 8, 0x20, 0x40, 0xE0, 0xFF); /* 下半:蓝 */
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
CHECK(rc == 0, "两色取色成功");
CHECK(m != s, "主色 != 次色");
/* 蓝的饱和权重更高(sat=192 vs 红 176)→ 主色偏蓝,次色偏红 */
CHECK(((m >> 16) & 0xFF) < 0x80 && (m & 0xFF) > 0xA0, "主色为蓝系(B 高)");
CHECK(((s >> 16) & 0xFF) > 0xA0 && (s & 0xFF) < 0x80, "次色为红系(R 高)");
free(buf);
}
static void c3_skip_near(void)
{
printf("C3 近黑/近白/透明跳过\n");
/* 近黑 + 一排鲜艳 → 主=鲜艳(近黑被跳过) */
int W = 12, H = 12;
unsigned char *buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, 11, 0x08, 0x08, 0x08, 0xFF); /* 近黑(luma<24) */
fill_rect_px(buf, W, 0, 11, W, 1, 0xE0, 0x30, 0x40, 0xFF); /* 鲜艳一行 */
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor_rgba(buf, W, H, &m, &s);
CHECK(rc == 0, "近黑+鲜艳 → 成功");
CHECK(m == 0xE03040, "主色==鲜艳色(近黑被跳过)");
free(buf);
/* 全近黑 → -1(回退主题) */
buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, H, 0x10, 0x10, 0x10, 0xFF);
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全近黑 → -1");
free(buf);
/* 全近白 → -1 */
buf = make_rgba(W, H);
fill_rect_px(buf, W, 0, 0, W, H, 0xF8, 0xF8, 0xF8, 0xFF);
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全近白 → -1");
free(buf);
/* 全透明 → -1 */
buf = make_rgba(W, H);
/* 默认 calloc → rgba=0 → alpha=0 全部跳过 */
CHECK(meow_img_duocolor_rgba(buf, W, H, &m, &s) == -1, "全透明 → -1");
free(buf);
}
static void c4_size_invariant(void)
{
printf("C4 尺寸缩放/副采样不变性\n");
/* 12×12 vs 120×120:同内容上下半红蓝 → 主色(蓝)一致 */
uint32_t m1 = 0, s1 = 0, m2 = 0, s2 = 0;
unsigned char *b1 = make_rgba(12, 12);
fill_rect_px(b1, 12, 0, 0, 12, 6, 0xE0, 0x30, 0x40, 0xFF);
fill_rect_px(b1, 12, 0, 6, 12, 6, 0x20, 0x40, 0xE0, 0xFF);
CHECK(meow_img_duocolor_rgba(b1, 12, 12, &m1, &s1) == 0, "小图取色成功");
unsigned char *b2 = make_rgba(120, 120);
fill_rect_px(b2, 120, 0, 0, 120, 60, 0xE0, 0x30, 0x40, 0xFF);
fill_rect_px(b2, 120, 0, 60, 120, 60, 0x20, 0x40, 0xE0, 0xFF);
CHECK(meow_img_duocolor_rgba(b2, 120, 120, &m2, &s2) == 0, "大图取色成功");
CHECK(m1 == m2, "大/小图主色一致(副采样不变性)");
free(b1);
free(b2);
}
static void c5_img_bridge(void)
{
printf("C5 meow_img_duocolor 桥接(局部 struct meow_img)\n");
int W = 4, H = 4;
struct meow_img im;
im.w = W; im.h = H;
im.rgba = make_rgba(W, H);
fill_rect_px(im.rgba, W, 0, 0, W, 2, 0xE0, 0x30, 0x40, 0xFF);
fill_rect_px(im.rgba, W, 0, 2, W, 2, 0x20, 0x40, 0xE0, 0xFF);
uint32_t m = 0, s = 0;
int rc = meow_img_duocolor((const meow_img *)&im, &m, &s);
CHECK(rc == 0, "meow_img 句柄取色成功");
CHECK(((m >> 16) & 0xFF) < 0x80 && (m & 0xFF) > 0xA0, "桥接主色为蓝系");
/* NULL → -1 */
CHECK(meow_img_duocolor(NULL, &m, &s) == -1, "NULL 句柄 → -1");
free(im.rgba);
}
static void c6_derive(void)
{
printf("C6 meow_img_color_derive 派生主题\n");
meow_theme th; memset(&th, 0, sizeof th);
th.primary = 0x204060; th.secondary = 0xE0B050; /* 深蓝主 + 琥珀次 */
meow_img_color_theme ct;
meow_img_color_derive(0x204060, 0xE0B050, &th, &ct);
/* 可读性:主深次亮 → 平均亮度偏中低 → 文字白 */
CHECK(ct.fg == 0xFFFFFF, "fg==白色(背景偏暗可读)");
CHECK(ct.accent == 0xE0B050, "accent==次色(琥珀)");
CHECK(ct.accent_fg == 0x1C1C1C, "accent_fg==深色(琥珀上可读图标)");
CHECK(ct.bg_top != ct.bg_bottom, "背景双色渐变(顶!=底)");
/* scrim=主色压暗:比原主色更暗(向黑靠) */
CHECK(((ct.lyr.scrim >> 16) & 0xFF) < 0x20, "lyr.scrim 主色压暗(R 递减)");
CHECK(ct.lyr.cur == 0xFFFFFF, "lyr.cur==fg(可读)");
CHECK(ct.lyr.scrim_alpha == 176, "lyr.scrim_alpha==176");
}
static void c7_set_colors_inject(void)
{
printf("C7 set_colors 注入/回退(lyric_view 层)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, NULL, NULL);
assert(lv);
uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF 纯白 */
/* 注入封面派生主题 */
meow_img_color_theme ct;
meow_img_color_derive(0x204060, 0xE0B050, meow_theme_get(), &ct);
meow_lyric_view_set_colors(lv, &ct.lyr);
CHECK(meow_lyric_view_cur_color(lv) == ct.lyr.cur, "注入后 cur==派生 fg(可读)");
/* gray 随覆盖 cur 自动递减(非白 cur 亦然:红色 cur → 灰阶=0xBB0000 级) */
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
c.cur = 0xFF0000;
meow_lyric_view_set_colors(lv, &c);
CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "覆盖 cur=红生效");
CHECK(meow_lyric_view_line_color(lv, 1) == 0xBB0000, "gray(1) 随覆盖 cur 递减(0xBB0000)");
/* 回退:NULL → 主题默认 */
meow_lyric_view_set_colors(lv, NULL);
CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 回退主题默认");
meow_lyric_view_free(lv);
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/imgcolor.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(0x57);
c1_solid();
c2_two_colors();
c3_skip_near();
c4_size_invariant();
c5_img_bridge();
c6_derive();
c7_set_colors_inject();
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+205
View File
@@ -0,0 +1,205 @@
/*
* test_meow_lyric.c — meow_lyric_* 宿主单测(2026-09-03,SDK 通用歌词组件批1)。
*
* 编译(宿主,meow_lyric 为纯 libc + pthread,无需 SDK 其它对象):
* gcc -Wall -Wextra -pthread -o test_meow_lyric test_meow_lyric.c meow_lyric.c -I.
* ./test_meow_lyric
*
* 覆盖矩阵:
* T1 解析基础 + 乱序自动按时间升序排序(原文行)
* T2 meow_lyric_line_at 边界:首戳前 / 首戳 / 区间 / 恰逢戳 / 末戳后 / 负值
* T3 翻译合并:按相同 timestamp 匹配合并;无匹配翻译行忽略
* T4 服务器字面 \n / \r 当换行(含 \r\n 折叠成单空格)
* T5 行上限截断:meow_lyric_set_max_lines 下调后 parse 超限行丢弃
* T6 时间戳小数位自适应:1 位(0.1s) / 2 位(厘秒) / 3 位(网易云毫秒) / >3 位截断到 ms
* T7 越界访问:line_text/line_tl 越界返回 "";line_time 越界返回 -1
* T8 meow_lyric_clear:清空后 count=0 / text="" / line_time=-1;空输入 parse 返回 0
* T9 折行 meow_lyric_wrap:中西文混排断行(英文按词、中文按字符)+ max_rows 截断
* + 单字符超宽防死循环 + width 回调为 NULL 时按字节近似兜底
*/
#include "meow_lyric.h"
#include <stdio.h>
#include <string.h>
static int g_fail = 0;
#define CHECK(cond, msg) do { \
if (cond) { printf(" PASS: %s\n", msg); } \
else { printf(" FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail = 1; } \
} while (0)
/* 折行宽测量:ASCII=1 单位,多字节(CJK 等)=2 单位(用于中西文断行测试)。 */
static int uw(const char *u, int size, void *ctx)
{
(void)size; (void)ctx;
int w = 0;
for (const char *p = u; *p; ) {
unsigned char c = (unsigned char)*p;
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
w += (c < 0x80) ? 1 : 2;
p += ch;
}
return w;
}
static void t1_basic(void)
{
printf("T1 解析基础 + 乱序按时间升序排序\n");
/* 输入乱序;期望按时间戳升序存(Line B@1000 < Line C@2000 < Line A@3000) */
const char *lrc = "[00:03.00]Line A\n[00:01.00]Line B\n[00:02.00]Line C\n";
int n = meow_lyric_parse(lrc, NULL);
CHECK(n == 3, "count==3");
CHECK(meow_lyric_count() == 3, "count()==3");
CHECK(strcmp(meow_lyric_line_text(0), "Line B") == 0, "idx0==Line B (1000ms 升序最前)");
CHECK(strcmp(meow_lyric_line_text(1), "Line C") == 0, "idx1==Line C");
CHECK(strcmp(meow_lyric_line_text(2), "Line A") == 0, "idx2==Line A (3000ms 最后)");
CHECK(meow_lyric_line_time(0) == 1000, "line_time(0)==1000");
CHECK(meow_lyric_line_time(2) == 3000, "line_time(2)==3000");
}
static void t2_line_at(void)
{
printf("T2 meow_lyric_line_at 边界\n");
const char *lrc = "[00:01.00]a\n[00:02.00]b\n[00:03.00]c\n";
meow_lyric_parse(lrc, NULL);
CHECK(meow_lyric_line_at(500) == 0, "首戳前(500ms)→0");
CHECK(meow_lyric_line_at(1000) == 0, "恰逢首戳→0");
CHECK(meow_lyric_line_at(1500) == 0, "首区间(1500ms)→0");
CHECK(meow_lyric_line_at(2500) == 1, "第二区间(2500ms)→1");
CHECK(meow_lyric_line_at(3000) == 2, "恰逢末戳→2");
CHECK(meow_lyric_line_at(99999) == 2, "末戳后→n-1");
CHECK(meow_lyric_line_at(-100) == 0, "负值→0");
}
static void t3_translation(void)
{
printf("T3 翻译合并(同 timestamp;无匹配翻译行忽略)\n");
const char *lrc = "[00:02.00]Hello\n[00:05.00]World\n";
const char *tlrc = "[00:05.00]你好\n[00:02.00]你好世界\n[01:00.00]no match line\n";
int n = meow_lyric_parse(lrc, tlrc);
CHECK(n == 2, "count==2(原文行数)");
CHECK(strcmp(meow_lyric_line_text(0), "Hello") == 0, "idx0原文 Hello");
CHECK(strcmp(meow_lyric_line_tl(0), "你好世界") == 0, "idx0翻译 你好世界(按 2000ms 合并)");
CHECK(strcmp(meow_lyric_line_text(1), "World") == 0, "idx1原文 World");
CHECK(strcmp(meow_lyric_line_tl(1), "你好") == 0, "idx1翻译 你好(按 5000ms 合并)");
/* 01:00.00 无对应原文行 → 丢弃,不新增行也不污染其它翻译 */
CHECK(meow_lyric_count() == 2, "无匹配翻译行被丢弃(仍 2 行)");
}
static void t4_literal_newline(void)
{
printf("T4 服务器字面 \\n / \\r 当换行\n");
/* C 源码里 \\n 是字面反斜杠+n(服务器塞进一行的多行文本) */
const char *lrc = "[00:01.00]line1\\nline2\r\n[00:02.00]a\\r\\nb\n";
meow_lyric_parse(lrc, NULL);
CHECK(strcmp(meow_lyric_line_text(0), "line1 line2") == 0, "字面 \\n → 空格");
CHECK(strcmp(meow_lyric_line_text(1), "a b") == 0, "字面 \\r\\n 成对 → 单空格");
}
static void t5_limit(void)
{
printf("T5 行上限截断(set_max_lines 下调后超限丢弃)\n");
meow_lyric_set_max_lines(2);
const char *lrc = "[00:01]a\n[00:02]b\n[00:03]c\n[00:04]d\n";
int n = meow_lyric_parse(lrc, NULL);
CHECK(n == 2, "count==2(上限 2)");
CHECK(strcmp(meow_lyric_line_text(0), "a") == 0, "idx0==a");
CHECK(strcmp(meow_lyric_line_text(1), "b") == 0, "idx1==b(第三个 c 被截断)");
meow_lyric_set_max_lines(MEOW_LYRIC_LINES_MAX); /* 恢复默认 */
n = meow_lyric_parse(lrc, NULL);
CHECK(n == 4, "恢复上限后 count==4");
/* set_max_lines 越界 clamp */
meow_lyric_set_max_lines(0);
n = meow_lyric_parse(lrc, NULL);
CHECK(n == 1, "set_max_lines(0)→clamp=1,count==1");
meow_lyric_set_max_lines(MEOW_LYRIC_LINES_MAX);
}
static void t6_timefrac(void)
{
printf("T6 时间戳小数位自适应\n");
const char *lrc =
"[00:01.5]a\n" /* 1 位 → 1500ms */
"[00:02.25]b\n" /* 2 位 → 2250ms */
"[00:03.456]c\n" /* 3 位(网易云毫秒) → 3456ms */
"[00:04.1234]d\n"; /* >3 位截断到 ms → 4123ms */
meow_lyric_parse(lrc, NULL);
CHECK(meow_lyric_line_time(0) == 1500, "1 位小数 → 1500ms");
CHECK(meow_lyric_line_time(1) == 2250, "2 位小数 → 2250ms");
CHECK(meow_lyric_line_time(2) == 3456, "3 位小数 → 3456ms");
CHECK(meow_lyric_line_time(3) == 4123, ">3 位小数截断 → 4123ms");
/* 元数据行 [ti:..] 应跳过,不计入行(与歌词须在独立行;ncm 语义:首个非时间标签即令该行失效) */
meow_lyric_parse("[ti:Test]\n[ar:Someone]\n[00:01.00]x\n", NULL);
CHECK(meow_lyric_count() == 1, "元数据 [ti:..]/[ar:..] 行不被当作歌词");
}
static void t7_oob(void)
{
printf("T7 越界访问\n");
meow_lyric_parse("[00:01.00]x\n[00:02.00]y\n", NULL);
CHECK(strcmp(meow_lyric_line_text(-1), "") == 0, "line_text(-1)==\"\"");
CHECK(strcmp(meow_lyric_line_text(99), "") == 0, "line_text(99)==\"\"");
CHECK(strcmp(meow_lyric_line_tl(-1), "") == 0, "line_tl(-1)==\"\"");
CHECK(strcmp(meow_lyric_line_tl(99), "") == 0, "line_tl(99)==\"\"");
CHECK(meow_lyric_line_time(-1) == -1, "line_time(-1)==-1");
CHECK(meow_lyric_line_time(99) == -1, "line_time(99)==-1");
}
static void t8_clear_and_empty(void)
{
printf("T8 clear / 空输入\n");
meow_lyric_parse("[00:01.00]x\n", NULL);
meow_lyric_clear();
CHECK(meow_lyric_count() == 0, "clear 后 count==0");
CHECK(strcmp(meow_lyric_line_text(0), "") == 0, "clear 后 line_text(0)==\"\"");
CHECK(meow_lyric_line_time(0) == -1, "clear 后 line_time(0)==-1");
CHECK(meow_lyric_parse(NULL, NULL) == 0, "parse(NULL,NULL)→0");
CHECK(meow_lyric_count() == 0, "空解析后 count==0");
}
static void t9_wrap(void)
{
printf("T9 折行 meow_lyric_wrap\n");
char seg[8][MEOW_LYRIC_SEG_SZ];
/* 中西文:英文按词断行、中文按字符断行 → "Hello" / "世界" 两段 */
int r = meow_lyric_wrap("Hello 世界", 28, 6, seg, 8, uw, NULL);
CHECK(r == 2, "中西文折为 2 段");
CHECK(strcmp(seg[0], "Hello") == 0, "seg0==Hello(词边界断)");
CHECK(strcmp(seg[1], "世界") == 0, "seg1==世界(中文按字符)");
/* max_rows 截断:每单词一 bar,max_rows=3 → 只出 3 段 */
r = meow_lyric_wrap("a b c d e f g", 28, 1, seg, 3, uw, NULL);
CHECK(r == 3, "max_rows=3 截断为 3 段");
/* 单字符超宽(maxw=0):能放一个字符且不死循环,返回完整逐字符段 */
r = meow_lyric_wrap("abc", 28, 0, seg, 5, uw, NULL);
CHECK(r == 3, "maxw=0 逐字符折为 3 段(无死循环)");
CHECK(strcmp(seg[0], "a") == 0 && strcmp(seg[2], "c") == 0, "seg0==a / seg2==c");
/* width 回调 NULL → 字节近似兜底(maxw 作字节预算) */
r = meow_lyric_wrap("abcd", 28, 2, seg, 4, NULL, NULL);
CHECK(r == 2, "width=NULL 按字节近似折为 2 段");
CHECK(strcmp(seg[0], "ab") == 0 && strcmp(seg[1], "cd") == 0, "seg0==ab / seg1==cd");
/* 空/超短:txt 空 → 1 段空串 */
r = meow_lyric_wrap("", 28, 10, seg, 4, uw, NULL);
CHECK(r == 1 && seg[0][0] == 0, "空文本 → 1 段空串");
CHECK(meow_lyric_wrap(NULL, 28, 10, seg, 4, uw, NULL) == 1, "NULL 文本 → 1 段");
}
int main(void)
{
printf("== meow_lyric 宿主单测 ==\n");
t1_basic();
t2_line_at();
t3_translation();
t4_literal_newline();
t5_limit();
t6_timefrac();
t7_oob();
t8_clear_and_empty();
t9_wrap();
printf("%s\n", g_fail ? "== FAIL ==" : "== PASS ==");
return g_fail ? 1 : 0;
}
+319
View File
@@ -0,0 +1,319 @@
/*
* test_meow_lyric_view.c — meow_lyric_view(SDK 通用歌词页控件)宿主单测(2026-09-03,批2)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global;
* 另链批1 meow_lyric.c 提供歌词数据;meow_lyric_view.c 为被测控件):
* gcc -Wall -Wextra -o test_meow_lyric_view test_meow_lyric_view.c test_host_stub.c \
* meow_lyric.c meow_lyric_view.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_lyric_view
*
* 覆盖矩阵(对齐任务批2:活动行纯白/灰阶、翻译行占位不叠行、偏上锚定、折行中西文、点行 seek 命中、
* 主题换色、scrim 像素渲染):
* L1 活动行纯白 + 上下行逐级灰阶单调递减(dark 主题:onSurface=0xFFFFFF 纯白)
* L2 翻译行占位不叠行:3 行原文+翻译 → 活动行块高 bh=rw+1,下一块始于翻译行之后(块间无重叠)
* L3 偏上锚定:活动行原文块中心 = 视图中线 - above_off(严格偏上)
* L4 折行中西文:长行折成多段(rw>1),且块间按子行高排列
* L5 点行 seek 命中:命中=布局一致(点块内/块外就近取整;点翻译行归本行不误触下一行)
* L6 主题换色:set_colors 覆盖活动行/灰阶/背景;NULL 恢复主题默认
* L7 scrim 像素渲染:draw 后背底像素≈scrim 色(fill_rect_alpha 真实落地)
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
#include "meow_lyric_view.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
/* on_seek 回调收(验证 widget hit 路径回调接线)。 */
static int64_t g_seek_ms = -1;
static void seek_cb(int64_t ms, void *ctx) { (void)ctx; g_seek_ms = ms; }
/* ---- back_buf 像素读取(断言真实渲染) ---- */
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
/* 折行宽测量(宿主):ASCII=1 单位,CJK 多字节=2 单位(中西文断行用)。 */
static int lw(const char *u, int size, void *ctx)
{
(void)size; (void)ctx;
int w = 0;
for (const char *p = u; *p; ) {
unsigned char c = (unsigned char)*p;
int ch = (c < 0x80) ? 1 : (c < 0xE0) ? 2 : (c < 0xF0) ? 3 : 4;
w += (c < 0x80) ? 1 : 2;
p += ch;
}
return w;
}
/* 亮度(灰度单调性判断用)。 */
static int lum(uint32_t c)
{
return ((c >> 16) & 255) * 3 + ((c >> 8) & 255) * 6 + (c & 255);
}
/*
* 测试歌词:7 行原创 + 两行翻译(与原文同戳合并)。下标:
* 0 First(1000) / 1 Second(2000,tl=第二句翻译) / 2 Third(3000) / 3 Fourth(4000)
* 4 Fifth(5000,tl=第五句翻译长文本折叠测试啊啊啊) / 5 Sixth(6000) / 6 Seventh(7000)
*/
static void load_lyric(void)
{
const char *lrc =
"[00:01.00]First\n"
"[00:02.00]Second\n"
"[00:03.00]Third\n"
"[00:04.00]Fourth\n"
"[00:05.00]Fifth\n"
"[00:06.00]Sixth\n"
"[00:07.00]Seventh\n";
const char *tlrc =
"[00:02.00]\u7b2c\u4e8c\u53e5\u7ffb\u8bd1\n"
"[00:05.00]\u7b2c\u4e94\u53e5\u7ffb\u8bd1\u957f\u6587\u672c\u6298\u53e0\u6d4b\u8bd5\u554a\u554a\u554a\n";
int n = meow_lyric_parse(lrc, tlrc);
assert(n == 7);
/* 翻译合并校验(批1 语义,作前置保证) */
assert(strcmp(meow_lyric_line_tl(1), "\u7b2c\u4e8c\u53e5\u7ffb\u8bd1") == 0);
assert(meow_lyric_line_tl(2)[0] == 0);
}
static void l1_white_gray(void)
{
printf("L1 活动行纯白 + 灰阶单调递减\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
const meow_theme *th = meow_theme_get();
/* 活动行 = 主题 onSurface(dark=0xFFFFFF 纯白) */
uint32_t cur = meow_lyric_view_cur_color(lv);
CHECK(cur == th->onSurface, "cur == th->onSurface (dark=0xFFFFFF 纯白)");
CHECK((cur & 0xFFFFFF) == 0xFFFFFF, "cur 为纯白 0xFFFFFF");
CHECK(meow_lyric_view_line_color(lv, 0) == cur, "line_color(0)==cur");
/* 灰阶单调递减(距活动行越远越灰) */
int mono = 1;
for (int d = 1; d <= 5; d++)
if (lum(meow_lyric_view_line_color(lv, d)) >= lum(meow_lyric_view_line_color(lv, d - 1))) mono = 0;
CHECK(mono, "gray dist 1..5 亮度单调递减");
/* 最远灰阶(对齐 ncm 0x555555) */
CHECK(meow_lyric_view_line_color(lv, 5) == 0x555555, "gray(5)==0x555555 (ncm lyr_gray_tbl)");
meow_lyric_view_free(lv);
}
static void l2_translation_block(void)
{
printf("L2 翻译行占位不叠行(3 行原文 + 翻译)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt > 0, "可见条目数 > 0");
/* 找出当前行(idx1)所在块与下一块 */
int ci = -1, next = -1;
for (int k = 0; k < cnt; k++) {
if (vis[k].ent == 1) ci = k;
else if (ci >= 0 && next < 0) next = k; /* 当前块之后的第一块 */
}
CHECK(ci >= 0, "当前行(Second)在可见块中");
CHECK(vis[ci].bh == vis[ci].rw + 1, "当前行块高 bh=rw+1(原文折行 + 翻译行占 1 子行)");
if (next >= 0) {
/* 下一块顶 = 当前块顶 + 当前块高×子行高(块间无重叠、翻译行不叠进下一行) */
CHECK(vis[next].y == vis[ci].y + vis[ci].bh * 44, "下一块始于翻译行之后(无叠行)");
} else {
/* 当前行是最后可视块:其块高应把翻译行算进去(翻译行占位,否则行会被截断) */
CHECK(vis[ci].y + vis[ci].bh * 44 <= 720, "当前块含翻译行(占用垂直空间)");
}
meow_lyric_view_free(lv);
}
static void l3_above_anchor(void)
{
printf("L3 偏上锚定(活动行块中心在视图中线上方)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */
assert(lv);
meow_lyric_view_set_pos(lv, 5000); /* cur = idx4 Fifth(5 行之后,上方充足) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 5000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt > 0, "可见条目数 > 0");
int ci = -1;
for (int k = 0; k < cnt; k++) if (vis[k].ent == 4) { ci = k; break; }
CHECK(ci >= 0, "当前行(Fifth)在可见块中");
/* 活动行原文块中心 = 块顶 + rw×子行高/2 = 视图中线 - above_off(严格偏上 50px) */
int center = vis[ci].y + vis[ci].rw * 44 / 2;
CHECK(center == 350 - 50, "活动行块中心=350-50=300(偏上锚定 above_off=50)");
CHECK(center < 350, "活动行块中心在视图中线(350)偏上");
meow_lyric_view_free(lv);
}
static void l4_wrap_cjk_west(void)
{
printf("L4 折行中西文(长行折多段,块间按子行高排列)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
/* 窄视图强制折行:w=120 → maxw=120-48=72 单位 */
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 120, 720, lw, NULL);
assert(lv);
/* 用长英文+中文混排行,使当前行折成 >1 段 */
meow_lyric_parse(
"[00:01.00]The quick brown fox jumps over the lazy dog and runs far away into the woods to find shelter from the cold rain today\n"
"[00:05.00]\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u4e2d\u6587\u8bba\u6587\u6d4b\u8bd5\u554a\u554a\u554a\u554a\n",
NULL);
meow_lyric_view_set_pos(lv, 1000);
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 1000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt >= 2, "该行折成多段(可见条目含上下行)");
/* 当前行(idx0,长文本)应从 1 段折成多段 */
int ci = -1;
for (int k = 0; k < cnt; k++) if (vis[k].ent == 0) { ci = k; break; }
CHECK(ci >= 0, "当前行在可见块中");
CHECK(vis[ci].rw > 1, "长行折成 rw>1 段(中西文按 词/字符 断行)");
/* 块内子行间距为 sub_lh:子行间不重叠(块高=rw 子行) */
CHECK(vis[ci].bh == vis[ci].rw, "无翻译当前行 bh=rw(子行数=折行段数)");
/* 恢复 7 行歌词(供后续用例) */
load_lyric();
meow_lyric_view_free(lv);
}
static void l5_hit_line(void)
{
printf("L5 点行 seek 命中 = 布局一致\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 100, 480, 500, lw, NULL); /* 视图中线=350 */
assert(lv);
meow_lyric_view_set_pos(lv, 2000); /* cur = idx1 Second(有翻译) */
meow_lyric_view_vis vis[MEOW_LYRIC_VIEW_MAX_ROWS];
int cnt = meow_lyric_view_visible(lv, 2000, vis, MEOW_LYRIC_VIEW_MAX_ROWS);
CHECK(cnt > 0, "可见条目数 > 0");
/* 命中必须与绘制布局一致:点每个可见块内子行,命中该块 ent */
int consistent = 1;
for (int k = 0; k < cnt && consistent; k++) {
int yb = vis[k].y, yh = vis[k].bh * 44;
for (int dy = 0; dy < yh && consistent; dy += 4) { /* 块内等距采样 */
int hit = meow_lyric_view_hit_line(lv, 240, yb + dy);
if (hit != vis[k].ent) consistent = 0;
}
}
CHECK(consistent, "命中=布局(块内逐点命中 ent 一致)");
/* 点翻译行(当前行块内下一子行)归本行,不误触下一行 */
int ci = -1;
for (int k = 0; k < cnt; k++) if (vis[k].ent == 1) { ci = k; break; }
CHECK(ci >= 0, "当前行在可见块中");
if (ci >= 0 && vis[ci].bh > vis[ci].rw) { /* 有翻译行 */
int ty = vis[ci].y + vis[ci].rw * 44 + 10; /* 翻译子行内 */
CHECK(meow_lyric_view_hit_line(lv, 240, ty) == 1, "点翻译行归本行(Second)不误触下一行");
}
/* 点可见块外(视图内)→ 就近取整(顶部/底部可见行) */
CHECK(meow_lyric_view_hit_line(lv, 240, 101) == vis[0].ent, "视图内顶缝→就近取第一可见块");
CHECK(meow_lyric_view_hit_line(lv, 240, 599) == vis[cnt - 1].ent, "视图内底缝→就近取最后可见块");
/* 视图外 → 未命中 */
CHECK(meow_lyric_view_hit_line(lv, 240, 50) == -1, "视图外(y=50)未命中");
CHECK(meow_lyric_view_hit_line(lv, 490, 300) == -1, "视图外(x=490)未命中");
/* widget 命中路径:命中当前行(有翻译)→ 触发 on_seek 回调 line_time(1)=2000 */
g_seek_ms = -1;
meow_lyric_view_set_on_seek(lv, seek_cb, NULL);
if (ci >= 0) {
int cy = vis[ci].y + 2; /* 当前行原文块内 */
CHECK(meow_widget_hit(meow_lyric_view_widget(lv), 240, cy) == 1, "widget hit 命中当前行");
CHECK(g_seek_ms == 2000, "on_seek 回调 line_time(Second)=2000");
} else {
CHECK(1, "on_seek 校验跳过(无当前行块)");
}
meow_lyric_view_free(lv);
}
static void l6_theme_switch(void)
{
printf("L6 主题换色(set_colors 覆盖 / NULL 恢复默认)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
uint32_t def_cur = meow_lyric_view_cur_color(lv); /* dark=0xFFFFFF */
/* 覆盖:活动行红、灰阶自定义、scrim 自定义 */
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
c.scrim = 0x1E1E22; c.cur = 0xFF0000; c.scrim_alpha = 255;
c.gray[0] = 0xFF0000; c.gray[1] = 0xCC0000; c.gray[2] = 0x990000;
c.gray[3] = 0x660000; c.gray[4] = 0x440000; c.gray[5] = 0x220000;
meow_lyric_view_set_colors(lv, &c);
CHECK(meow_lyric_view_cur_color(lv) == 0xFF0000, "set_colors 覆盖活动行为红");
CHECK(meow_lyric_view_line_color(lv, 3) == 0x660000, "set_colors 覆盖灰阶(3)=0x660000");
CHECK(meow_lyric_view_line_color(lv, 5) == 0x220000, "set_colors 覆盖灰阶(5)=0x220000");
/* 恢复默认:NULL → 回主题 onSurface 纯白 */
meow_lyric_view_set_colors(lv, NULL);
CHECK(meow_lyric_view_cur_color(lv) == def_cur, "set_colors(NULL) 恢复主题默认");
meow_lyric_view_free(lv);
}
static void l7_scrim_pixel(void)
{
printf("L7 scrim 像素渲染(draw 落地)\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_lyric_view *lv = meow_lyric_view_create(0, 0, 480, 720, lw, NULL);
assert(lv);
/* 不透明 scrim → 背底像素应为 scrim 色(RGB565 舍入容差) */
meow_lyric_view_colors c; memset(&c, 0, sizeof c);
c.scrim = 0x203040; c.scrim_alpha = 255;
meow_lyric_view_set_colors(lv, &c);
meow_lyric_view_set_pos(lv, 2000);
static const char *lrc =
"[00:01.00]First\n[00:02.00]Second\n[00:03.00]Third\n[00:04.00]Fourth\n";
meow_lyric_parse(lrc, NULL);
meow_ui_clear(0);
meow_widget *w = meow_lyric_view_widget(lv);
meow_widget_draw(w);
CHECK(px_near(240, 350, 0x20, 0x30, 0x40), "视图中线像素≈scrim 0x203040(文本打桩不影响 scrim)");
CHECK(px_near(50, 50, 0x20, 0x30, 0x40), "视图左上像素≈scrim 0x203040");
meow_lyric_view_free(lv);
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/lyricview.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(0x53);
load_lyric();
l1_white_gray();
l2_translation_block();
l3_above_anchor();
l4_wrap_cjk_west();
l5_hit_line();
l6_theme_switch();
l7_scrim_pixel();
meow_ui_activity_pop();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+171
View File
@@ -0,0 +1,171 @@
/* test_meow_md3.c — 宿主验证 MD3(Material You)对齐修复(agent-434 P0/P1/P3):
* P0 listview 容器模式(默认 renderer:行=surfaceContainer 圆角卡 + 行间空隙 + 去分隔线);
* P1 按钮胶囊化(圆角=h/2);
* P3 Switch off-track/off-thumb、NavigationBar active pill 64×32、Checkbox/Radio 视觉尺寸。
* 断言全部「画完读 back_buf 像素」(region_has/px_near),不依赖 /dev/fb0/字库(meow_font 打桩)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_md3 test_meow_md3.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_md3
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 12; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* Listview src2 数据源:2 行普通富 item(无图标、无右尾件,label-only)。 */
typedef struct { const char *title; const char *sub; int right; int enabled; } trow_t;
static trow_t g_rows[2] = {
{ "Row 0", NULL, MEOW_LIST_RIGHT_NONE, 1 },
{ "Row 1", NULL, MEOW_LIST_RIGHT_NONE, 1 },
};
static int trow_src(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= 2) return 0;
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = g_rows[idx].title;
out->subtitle = g_rows[idx].sub;
out->right = g_rows[idx].right;
out->on = 0;
out->enabled = g_rows[idx].enabled;
return 1;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/md3.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
assert(meow_ui_width() == 480 && meow_ui_height() == 720);
const meow_theme *th = meow_theme_get();
int sf_r, sf_g, sf_b, sc_r, sc_g, sc_b, p_r, p_g, p_b;
int pc_r, pc_g, pc_b, ov_r, ov_g, ov_b, sch_r, sch_g, sch_b, scs_r, scs_g, scs_b;
UNPK(th->surface, sf_r, sf_g, sf_b);
UNPK(th->surfaceContainer, sc_r, sc_g, sc_b);
UNPK(th->primary, p_r, p_g, p_b);
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
UNPK(th->outline, ov_r, ov_g, ov_b);
UNPK(th->surfaceContainerHighest, sch_r, sch_g, sch_b);
UNPK(th->secondaryContainer, scs_r, scs_g, scs_b);
/* ---- P0 listview 容器模式:行=surfaceContainer 圆角卡 + 空隙,去分隔线 ---- */
meow_widget *lv = meow_listview_create(20, 240, 320, 112, 2, trow_src, NULL, MEOW_LIST_SEL_SINGLE);
assert(lv);
assert(meow_listview_set_item_h(lv, 56) == 56);
assert(meow_listview_set_container(lv, 1) == 0);
meow_ui_clear(th->surface);
meow_widget_draw(lv);
/* 行 0 卡(x=28,y=244,w=304,h=48):中心=surfaceContainer;行 1 同。 */
assert(px_near(180, 268, sc_r, sc_g, sc_b));
assert(px_near(180, 324, sc_r, sc_g, sc_b));
/* 卡间空隙(y=292..300):surface(行间分隔用空隙)。 */
assert(px_near(180, 296, sf_r, sf_g, sf_b));
/* 去分隔线:行边界不再画 outlineVariant(改空隙代替)。 */
assert(!px_near(180, 292, ov_r, ov_g, ov_b));
/* 选中行 0 → primaryContainer(tonal,非满幅实心)。 */
meow_listview_set_sel(lv, 0);
meow_ui_clear(th->surface);
meow_widget_draw(lv);
assert(px_near(180, 268, pc_r, pc_g, pc_b));
assert(px_near(180, 324, sc_r, sc_g, sc_b)); /* 行 1 未选仍卡 */
printf("PASS listview container: rounded card + gap + no-divider + tonal select\n");
/* ---- P1 按钮胶囊化:圆角=h/2(56/2=28)---- */
meow_widget *btn = meow_btn_create_variant(10, 10, 200, 56, "FILLED", MEOW_BTN_FILLED);
assert(btn);
meow_ui_clear(th->surface);
meow_widget_draw(btn);
assert(px_near(110, 22, p_r, p_g, p_b)); /* 上部(避开文字)=primary */
/* 顶缘中心在圆角胶囊内(仍 primary);角落(圆角外)回 surface。 */
assert(px_near(110, 13, p_r, p_g, p_b));
assert(px_near(12, 12, sf_r, sf_g, sf_b)); /* 角落被圆角切掉 */
/* 同高矮按钮(h=40 → 圆角 20):上部仍 primary,角落 surface。 */
meow_widget *btn2 = meow_btn_create_variant(10, 120, 120, 40, "B", MEOW_BTN_FILLED);
assert(btn2);
meow_ui_clear(th->surface);
meow_widget_draw(btn2);
assert(px_near(70, 125, p_r, p_g, p_b));
assert(px_near(12, 122, sf_r, sf_g, sf_b));
printf("PASS button: capsule radius = h/2\n");
/* ---- P3 Switch off-track/off-thumb ---- */
meow_widget *sw = meow_switch_create(20, 300, 0); /* off */
assert(sw);
meow_ui_clear(th->surface);
meow_widget_draw(sw);
assert(px_near(60, 316, sch_r, sch_g, sch_b)); /* off-track=surfaceContainerHighest */
assert(px_near(36, 316, sf_r, sf_g, sf_b)); /* off-thumb=surface(表面色,非恒白) */
meow_switch_set(sw, 1);
meow_ui_clear(th->surface);
meow_widget_draw(sw);
assert(px_near(28, 316, p_r, p_g, p_b)); /* on-track=primary(thumb 左侧) */
printf("PASS switch: off-track surfaceContainerHighest / off-thumb surface\n");
/* ---- P3 NavigationBar active pill 64×32 ---- */
meow_widget *nb = meow_navigationbar_create(0, 600, 480, 80);
assert(nb);
meow_navigationbar_add_item(nb, "A");
meow_navigationbar_add_item(nb, "B");
meow_navigationbar_add_item(nb, "C");
meow_navigationbar_add_item(nb, "D");
meow_navigationbar_set_selected(nb, 0);
meow_ui_clear(th->surface);
meow_widget_draw(nb);
/* item0 pill 中心 (ix+60, y+40)=(60,640) — 64×32 居中于栏;pill 内=secondaryContainer。 */
assert(px_near(60, 640, scs_r, scs_g, scs_b));
/* pill 外(栏上部)= surfaceContainer(navbar 容器底)。 */
assert(px_near(60, 610, sc_r, sc_g, sc_b));
printf("PASS navbar: active pill 64x32 (was 48x22)\n");
/* ---- P3 Checkbox 视觉 24dp(原 34 偏大):勾选框视觉居中,角落回 surface ---- */
meow_widget *ck = meow_checkbox_create(150, 60);
assert(ck);
meow_ui_clear(th->surface);
meow_widget_draw(ck);
/* 未选:24dp 描边框(左缘=150+5=155);框边=outline;内部=surface(空心)。 */
assert(px_near(155, 77, ov_r, ov_g, ov_b)); /* 框左缘 stroke */
assert(px_near(167, 77, sf_r, sf_g, sf_b)); /* 内部空心 */
meow_widget_set_selected(ck, 1);
meow_ui_clear(th->surface);
meow_widget_draw(ck);
/* 选中:24dp 实心 primary 居中(左缘 155..179 顶 65..89);控件矩形角落(视觉 24 外)不填。 */
assert(px_near(150, 60, sf_r, sf_g, sf_b)); /* 视觉 24 外 → surface */
assert(px_near(167, 77, p_r, p_g, p_b)); /* 视觉中心=primary */
printf("PASS checkbox: 24dp visual (was 34)\n");
meow_widget_free(lv); meow_widget_free(btn); meow_widget_free(btn2);
meow_widget_free(sw); meow_widget_free(nb); meow_widget_free(ck);
printf("ALL MD3 PASS\n");
return 0;
}
+196
View File
@@ -0,0 +1,196 @@
/* test_meow_ui_home_gesture.c — 批② SDK 全局「App 内底部热区上滑回桌面」宿主验证。
*
* 覆盖(并同 meow_ui_input_is_home_swipe 判据、touch_release 分类、meow_ui_run 分发三层):
* T1 判定矩阵(meow_ui_input_is_home_swipe,镜像 launcher np_gesture.h np_home_gesture):
* 底部热区上滑=1;底部左缘上滑=0(左缘返回互斥);中部上滑=0(App 滚动);底部横向=0;
* 底部下滑=0;上移不足=0;横向主导=0;屏底边界(down_y=fb_h-HOME_EDGE_H)之外=0。
* T2 触摸状态机分类(真实 touch_release → 队列 pop 断言):
* 底部热区上滑→MEOW_EV_HOME(a=MEOW_SWIPE_UP);中部上滑→MEOW_EV_SWIPE(UP)(不被剥回桌面);
* 底部左缘横滑→MEOW_EV_SWIPE(LEFT)(原左缘返回,不被剥)。
* T3 分发(meow_ui_run 主循环,run-loop 复用 test_meow_ui_view_event 的 driver 线程范式):
* 3a 未注册 on_swipe:注入 MEOW_EV_HOME → meow_ui_nav_request_exit 被触发(一次)——回桌面;
* 3b 未注册 on_swipe:注入 MEOW_EV_SWIPE(UP)(中部上滑)→ nav_request_exit 不触发(App 滚动);
* 3c 注册 on_swipe:注入 MEOW_EV_HOME → nav_request_exit 触发(系统级手势优先于 App on_swipe),
* on_swipe 不被调用——屏底热区上滑是系统保留手势,任意 App 内生效(用户 2026-09-04 批②)。
*
* 编译(宿主 gcc,链真实 SDK 源全集 + 打桩 font/global;**不链 meow_ui_nav.c**——本测以测试内
* meow_ui_nav_request_exit 桩计数验证触发):
* gcc -Wall -Wextra -o test_meow_ui_home_gesture test_meow_ui_home_gesture.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_home_gesture
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_input_touch_set/move/release + meow_input_pop + MEOW_EV_* */
#include "meow_ui_nav.h" /* meow_ui_nav_request_exit 原型(本测以桩计数验证) */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
/* MEOW_EV_HOME 是 meow_ui_input.c 内部常量(未入公共头,同 MEOW_EV_NOTIF_PULL 惯例),按同值声明。 */
#ifndef MEOW_EV_HOME
#define MEOW_EV_HOME 9
#endif
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* fb 高度(宿主 480x720) */
#define FBW 480
#define FBH 720
/* 判据常量镜像(np_gesture.h NP_HOME_EDGE_H=28 / NP_HOME_SWIPE=65 / NP_EDGE_W=24)。 */
#define H_EDGE 28
#define H_SWIPE 65
#define EDGE_W 24
/* ---- nav_request_exit 桩(不链 meow_ui_nav.c;计数验证 SDK 触发) ---- */
static int g_nav_exit_calls = 0;
void meow_ui_nav_request_exit(void) { g_nav_exit_calls++; }
/* ---- on_swipe 桩(App 接管验证) ---- */
static int g_swipe_calls = 0;
static int g_swipe_dir = -1;
static int g_swipe_dx = 0;
static void app_on_swipe(int dir, int dx, int dy) { (void)dy; g_swipe_calls++; g_swipe_dir = dir; g_swipe_dx = dx; }
/* 从队列 pop 一个事件并回填 (type,a,b);无事件返回 0。 */
static int pop_ev(int *type, int *a, int *b)
{
meow_input_ev e;
if (!meow_input_pop(&e)) return 0;
*type = e.type; *a = e.a; *b = e.b;
return 1;
}
/* ============ T1 判定矩阵(meow_ui_input_is_home_swipe) ============ */
static void test_predicate(void)
{
printf("T1 meow_ui_input_is_home_swipe matrix\n");
int bz = FBH - H_EDGE; /* 底部热区上边界 y = 720-28 = 692 */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -120, FBH) == 1, "T1 bottom-hotzone up -> 1");
CHECK(meow_ui_input_is_home_swipe(100, FBH - 1, 20, -80, FBH) == 1, "T1 screen-bottom up (x>EDGE_W) -> 1");
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -H_SWIPE, FBH) == 1, "T1 exactly threshold up -> 1");
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, -30, -70, FBH) == 1, "T1 up w/ small left dx (adx<dy) -> 1");
/* 左缘互斥(down_x<EDGE_W) */
CHECK(meow_ui_input_is_home_swipe(10, bz + 10, 0, -120, FBH) == 0, "T1 left-edge origin -> 0 (edge-back reserved)");
/* 中部上滑(App 滚动) */
CHECK(meow_ui_input_is_home_swipe(240, 300, 0, -120, FBH) == 0, "T1 mid-screen up -> 0 (App scroll)");
/* 底部横向(左缘返回/横向逻辑保留) */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 120, 0, FBH) == 0, "T1 bottom horizontal -> 0");
/* 底部下滑 */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, 120, FBH) == 0, "T1 bottom down -> 0");
/* 上移不足(<H_SWIPE) */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, 0, -(H_SWIPE - 1), FBH) == 0, "T1 up below threshold -> 0");
/* 竖向未主导(横向位移≥上移量,adx>=dy) */
CHECK(meow_ui_input_is_home_swipe(240, bz + 10, -120, -80, FBH) == 0, "T1 up w/ big dx (not vertical-dominant) -> 0");
/* 热区边界之外(down_y < bz) */
CHECK(meow_ui_input_is_home_swipe(240, bz - 1, 0, -120, FBH) == 0, "T1 just above hotzone -> 0");
}
/* ============ T2 触摸状态机分类(touch_release → 队列) ============ */
static void test_touch_classify(void)
{
printf("T2 touch_release classification\n");
meow_input_drain();
/* 底部热区上滑 → MEOW_EV_HOME */
meow_input_touch_set(1, 240, FBH - 5, 0); /* 起点 y=715 ∈ [692,720) */
meow_input_touch_move(240, FBH - 5 - 130); /* 上移 130 */
meow_input_touch_release();
{
int t, a, b;
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_HOME && a == MEOW_SWIPE_UP,
"T2 bottom-hotzone up -> MEOW_EV_HOME(UP) got type=%d a=%d", t, a);
}
/* 中部上滑 → 普通 MEOW_EV_SWIPE(UP)(不被剥回桌面) */
meow_input_touch_set(1, 240, 300, 0);
meow_input_touch_move(240, 300 - 130);
meow_input_touch_release();
{
int t, a, b;
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_SWIPE && a == MEOW_SWIPE_UP,
"T2 mid up -> SWIPE(UP) got type=%d a=%d", t, a);
}
/* 底部左缘横滑 → 原有左缘返回(不被剥) */
meow_input_touch_set(1, 5, FBH - 5, 0);
meow_input_touch_move(5 + 100, FBH - 5);
meow_input_touch_release();
{
int t, a, b;
CHECK(pop_ev(&t, &a, &b) == 1 && t == MEOW_EV_SWIPE && a == MEOW_SWIPE_LEFT,
"T2 bottom-left horizontal -> SWIPE(LEFT) got type=%d a=%d", t, a);
}
}
/* ============ T3 分发(meow_ui_run 主循环) ============ */
static int g_inj_type, g_inj_a, g_inj_b;
static void *driver(void *arg)
{
(void)arg;
usleep(700000); /* 等 startup cooldown(500ms) 过后再注入 */
meow_input_inject(g_inj_type, g_inj_a, g_inj_b);
usleep(80000);
meow_ui_request_exit();
return NULL;
}
static void run_scenario(int inj_type, int inj_a, int inj_b, int register_swipe)
{
g_inj_type = inj_type; g_inj_a = inj_a; g_inj_b = inj_b;
g_nav_exit_calls = 0;
g_swipe_calls = 0; g_swipe_dir = -1; g_swipe_dx = 0;
meow_input_drain(); /* 防上一次残留事件影响本次分发 */
meow_ui_events ev; memset(&ev, 0, sizeof ev);
if (register_swipe) ev.on_swipe = app_on_swipe;
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
meow_ui_run(&ev);
pthread_join(thd, NULL);
}
static void test_dispatch(void)
{
printf("T3 meow_ui_run dispatch (home = nav_request_exit fallback)\n");
/* 3a 未注册 on_swipe:底部热区上滑 → nav_request_exit 触发一次(回桌面) */
run_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 0);
CHECK(g_nav_exit_calls == 1, "T3a no-on_swipe MEOW_EV_HOME -> nav_request_exit fired (got %d)", g_nav_exit_calls);
CHECK(g_swipe_calls == 0, "T3a no-on_swipe -> on_swipe NOT called");
/* 3b 未注册 on_swipe:中部上滑(普通 MEOW_EV_SWIPE UP)→ nav_request_exit 不触发(App 滚动) */
run_scenario(MEOW_EV_SWIPE, MEOW_SWIPE_UP, 120, 0);
CHECK(g_nav_exit_calls == 0, "T3b mid-swipe-up -> nav_request_exit NOT fired (App scroll), got %d", g_nav_exit_calls);
/* 3c 注册 on_swipe:底部热区上滑 → 系统优先,nav_request_exit 触发(回桌面),on_swipe 不被调用 */
run_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 1);
CHECK(g_nav_exit_calls == 1, "T3c on_swipe registered -> nav_request_exit STILL fired (system gesture wins), got %d", g_nav_exit_calls);
CHECK(g_swipe_calls == 0, "T3c system-priority home -> on_swipe NOT called");
}
int main(void)
{
meow_ui_opts o = { FBW, FBH };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
test_predicate();
test_touch_classify();
test_dispatch();
meow_ui_shutdown();
printf("home_gesture: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+121
View File
@@ -0,0 +1,121 @@
/* test_meow_ui_iconbtn.c — M7.0a 缺口#2 宿主验证:IconButton 圆形命中 + 按压态 + 位图图标。
*
* 覆盖:
* I1 圆形命中:中心/边界命中,角部/外缘不命中(区别于矩形按钮);
* I2 owner==active + enabled 双闸门:非 active 或禁用不命中;
* I3 按压态视觉:设 MEOW_WS_PRESSED 后重绘,圆心像素向 onSurface 变亮(与未按不同);
* 禁用时圆心向灰变暗;
* I4 实时按压(meow_input_touch 注入):按下命中圆心 → 圆心变亮;释放恢复。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_ui_iconbtn test_meow_ui_iconbtn.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_iconbtn
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* ---- back_buf 像素读取(断言真实渲染,不依赖目检) ---- */
static void rd_px(int x, int y, unsigned short *p)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *p = 0; return; }
if (bpp == 16) *p = *(unsigned short *)(bb + (size_t)y * line + (size_t)x * 2);
else *p = (unsigned short)(*(unsigned int *)(bb + (size_t)y * line + (size_t)x * 4) >> 16);
}
static unsigned short px565_at(int x, int y)
{
unsigned short p; rd_px(x, y, &p); return p;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/iconbtn.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(7); /* owner 对齐:IconButton 绑定 active=7 */
meow_theme_set_mode(MEOW_THEME_DARK);
/* size=64 → 外接框 (100,100,64,64) → 圆心 (132,132) r=32 */
meow_widget *ic = meow_iconbtn_create(100, 100, 64, MEOW_ICON_PLAY_ARROW);
assert(ic != NULL);
/* ============ I1 圆形命中 ============ */
printf("I1 circular hit\n");
CHECK(meow_widget_hit(ic, 132, 132), "center hits");
CHECK(meow_widget_hit(ic, 100, 132), "left edge (boundary) hits");
CHECK(meow_widget_hit(ic, 132, 100), "top edge (boundary) hits");
CHECK(!meow_widget_hit(ic, 100, 100), "corner (dist>r) MISSES (rect would hit)");
CHECK(!meow_widget_hit(ic, 132, 165), "below center (dist 33>32) MISSES");
/* ============ I2 owner 闸门:切 active 后不命中 ============ */
printf("I2 owner gate\n");
meow_ui_activity_push(8); /* 换 active:original owner=7 ≠ active=8 */
CHECK(!meow_widget_hit(ic, 132, 132), "non-active owner -> MISS (gate)");
meow_ui_activity_pop(); /* 回 active=7 */
CHECK(meow_widget_hit(ic, 132, 132), "back to active -> hits");
/* enabled 闸门 */
meow_widget_set_enabled(ic, 0);
CHECK(!meow_widget_hit(ic, 132, 132), "disabled -> MISS");
meow_widget_set_enabled(ic, 1);
CHECK(meow_widget_hit(ic, 132, 132), "enabled -> hits");
/* ============ I3 按压态视觉(状态机位 + 禁用变暗) ============
* 采样点选 (162,132):圆内 (r=30<32) 但超出图标 ink 右缘(约 x≤148)——取的是圆形底色,
* 而非图标墨色(圆心为图标覆盖,不适合测底色 tint)。 */
printf("I3 pressed visual (state machine / disabled)\n");
const meow_theme *th = meow_theme_get();
meow_ui_clear(th->bg);
unsigned short p_bg = px565_at(100, 100); /* 空屏角部 = 页面底色 */
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0); /* 未按 */
meow_widget_draw(ic);
unsigned short p_idle = px565_at(162, 132);
meow_widget_set_state(ic, MEOW_WS_PRESSED, 1); /* 按下 */
meow_widget_draw(ic);
unsigned short p_press = px565_at(162, 132);
CHECK(p_press != p_idle, "pressed circle-bg pixel differs from idle (tint layer)");
/* 禁用:向灰变暗,圆 bg 应更接近 onSurface 混合(与 idle 不同且更亮/更灰) */
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0);
meow_widget_set_enabled(ic, 0);
meow_widget_draw(ic);
unsigned short p_dis = px565_at(162, 132);
CHECK(p_dis != p_idle, "disabled circle-bg pixel differs from idle (dim)");
meow_widget_set_enabled(ic, 1);
/* 角部(圆外)不应出现圆形底色:等于页面底色(圆形命中/填充不外溢) */
meow_widget_draw(ic);
CHECK(px565_at(100, 100) == p_bg, "corner (outside circle) = page bg (no stray fill)");
/* ============ I4 实时按压(meow_input_touch 注入) ============ */
printf("I4 live press via input touch\n");
meow_widget_set_state(ic, MEOW_WS_PRESSED, 0);
meow_ui_clear(th->bg);
meow_widget_draw(ic);
unsigned short q_idle = px565_at(162, 132);
meow_input_touch_set(1, 132, 132, 0); /* 按在圆心 */
meow_widget_draw(ic);
unsigned short q_live = px565_at(162, 132);
CHECK(q_live != q_idle, "live touch press -> circle bg brightens");
meow_input_touch_set(0, 0, 0, 0); /* 释放 */
meow_widget_draw(ic);
CHECK(px565_at(162, 132) == q_idle, "release -> circle bg restores");
meow_widget_free(ic);
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+278
View File
@@ -0,0 +1,278 @@
/* test_meow_ui_lazygrid.c — 宿主验证 §12.4 2 维虚拟化宫格(meow_lazygrid):
* T1 虚拟化正确性:count/cols/max_scroll;首帧绑定可见行 item;scroll 下滚仅补新入视口的 item;
* 回滚不重绑;全量扫过 = count(每 index 恰 bind 一次);命中把滚动+网格坐标映射回 idx 正确。
* T2 默认 renderer:圆角卡片底 / 标题/副标题 / 选中高亮(primaryContainer)/ 禁用变淡。
* T3 完全自绘 set_item_draw:每可见格调一次 + 自绘内容像素。
* T4 空态占位(count=0 + empty_text 居中)。
* T5 meow_scroll 集成(meow_lazygrid_scroll_attach:track/fling/snap 到行边界)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -o test_meow_ui_lazygrid test_meow_ui_lazygrid.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_lazygrid
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp:真实像素断言 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
/* ---- back_buf 像素读取(真实渲染断言) ---- */
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
static int region_has(int x, int y, int w, int h, int r, int g, int b)
{
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px_near(x + dx, y + dy, r, g, b)) cnt++;
return cnt;
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* ---- 网格几何(测试统一) ---- */
#define GX 20
#define GY 50
#define GW 440
#define GH 200
#define COLS 3
#define CW 100
#define CH 80
#define GAP 10
#define ROWH (CH + GAP) /* 90 */
#define N 20 /* 默认总项数 */
static int g_count_override; /* <0 = 用默认 N;>=0 = 覆盖(空态/小列表) */
static int g_bind_total; /* bind 回调计数(含 hit 的临时 bind) */
static int g_draw_total; /* draw_cb(自绘)计数 */
static int g_disabled_at; /* 该 index 返回 enabled=0(-1=无) */
static int g_tap_cb_idx; /* tap_at 回调记录的选中项 */
static void lg_tap_cb(int idx, void *ctx) { (void)ctx; g_tap_cb_idx = idx; }
static int lg_count(void *ctx) { (void)ctx; return g_count_override >= 0 ? g_count_override : N; }
static void lg_bind(int idx, meow_lazygrid_item *it, void *ctx)
{
(void)ctx;
g_bind_total++;
memset(it, 0, sizeof *it);
static char t[32], s[32];
snprintf(t, sizeof t, "cell %d", idx);
snprintf(s, sizeof s, "st %d", idx);
it->title = t; it->subtitle = s;
it->enabled = 1;
if (g_disabled_at == idx) it->enabled = 0;
}
/* ---- T3 完全自绘:每格画实心块 + 角标记 ---- */
static void mydraw(int idx, int x, int y, int w, int h, int st, void *ctx)
{
(void)idx; (void)ctx;
g_draw_total++;
meow_ui_fill_rect(x, y, w, h, 0x334455); /* 自定义格底 */
meow_ui_fill_rect(x + 4, y + 4, 8, 8, 0xFF0000); /* 角标记 */
if (st & MEOW_LAZYGRID_ST_SELECTED) meow_ui_fill_rect(x + w - 12, y + 4, 8, 8, 0x00FF00);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
int sc_r, sc_g, sc_b, on_r, on_g, on_b, pc_r, pc_g, pc_b, osv_r, osv_g, osv_b, opc_r, opc_g, opc_b;
UNPK(th->surfaceContainer, sc_r, sc_g, sc_b);
UNPK(th->onSurface, on_r, on_g, on_b);
UNPK(th->primaryContainer, pc_r, pc_g, pc_b);
UNPK(th->onSurfaceVariant, osv_r, osv_g, osv_b);
UNPK(th->onPrimaryContainer, opc_r, opc_g, opc_b);
g_count_override = -1; g_bind_total = 0; g_disabled_at = -1;
/* ============ T1 虚拟化正确性 ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
CHECK(g != NULL, "create");
CHECK(meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL) == 0, "vsource");
CHECK(meow_lazygrid_max_scroll(g) == 7 * CH + 6 * GAP - GH, "max_scroll %d (expect %d)",
meow_lazygrid_max_scroll(g), 7 * CH + 6 * GAP - GH); /* 620-200=420 */
CHECK(meow_lazygrid_scroll(g) == 0, "scroll0");
meow_ui_clear(th->bg);
meow_lazygrid_draw(g, th);
int B0 = g_bind_total;
int vis_rows = GH / ROWH + 1; /* 3 */
int expect0 = vis_rows * COLS; /* 9(row0..2 × 3 格) */
CHECK(B0 == expect0, "first draw binds %d got %d", expect0, B0);
/* 下滚 1 行(90px):新入视口 = 末 1 行 3 格 → bind +3(其余同槽不重绑) */
meow_lazygrid_set_scroll(g, ROWH);
meow_lazygrid_draw(g, th);
int B1 = g_bind_total;
CHECK(B1 == B0 + COLS, "scroll+1row binds +%d got %d (was %d)", COLS, B1 - B0, B0);
/* 回滚到顶:槽仍在 → 不重绑(此段不调 hit,避免临时 bind 干扰计数) */
meow_lazygrid_set_scroll(g, 0);
meow_lazygrid_draw(g, th);
CHECK(g_bind_total == B1, "scroll-back no rebind got %d (was %d)", g_bind_total, B1);
/* 全量扫过:每个 index 进入视口时 bind 恰一次 → 总 == N(有界,非 per-frame × count) */
int prev = 0;
while (prev <= N) {
int off = prev * ROWH;
meow_lazygrid_set_scroll(g, off);
meow_lazygrid_draw(g, th);
if (off >= meow_lazygrid_max_scroll(g)) break;
prev++;
}
CHECK(g_bind_total == N, "full sweep binds each index once got %d (expect %d)", g_bind_total, N);
/* 命中:索引 = 滚动 + 网格坐标映射(此后的 hit 走临时 bind,不影响上面的总数断言) */
meow_lazygrid_set_scroll(g, 0);
CHECK(meow_lazygrid_hit(g, GX + 10, GY + 10) == 0, "hit cell0 got %d", meow_lazygrid_hit(g, GX + 10, GY + 10));
meow_lazygrid_set_scroll(g, ROWH);
/* scroll=90:GY+10(视口顶)→ 内容 y=100 → row1([90,170])col0 → idx=3 */
CHECK(meow_lazygrid_hit(g, GX + 10, GY + 10) == COLS,
"hit maps scroll+grid got %d (expect 3)", meow_lazygrid_hit(g, GX + 10, GY + 10));
/* gap 区不命中 */
CHECK(meow_lazygrid_hit(g, GX + CW + GAP / 2, GY + 10) == -1, "horizontal gap miss got %d",
meow_lazygrid_hit(g, GX + CW + GAP / 2, GY + 10));
/* 越界列(row0 左侧无格/第 4 列)不命中 */
meow_lazygrid_set_scroll(g, 0);
CHECK(meow_lazygrid_hit(g, GX + COLS * (CW + GAP) + 10, GY + 10) == -1, "col overflow miss got %d",
meow_lazygrid_hit(g, GX + COLS * (CW + GAP) + 10, GY + 10));
CHECK(meow_lazygrid_hit(g, GX + 10, GY + ROWH) == COLS, "hit row1 col0 idx=3 got %d",
meow_lazygrid_hit(g, GX + 10, GY + ROWH));
/* tap_at:命中 → 置选中 + 回调 */
g_tap_cb_idx = -1;
meow_lazygrid_set_on_tap(g, lg_tap_cb, NULL);
int tap_idx = meow_lazygrid_tap_at(g, GX + 50, GY + 40);
CHECK(tap_idx == 0, "tap_at idx0 got %d", tap_idx);
CHECK(meow_lazygrid_sel(g) == 0, "sel set to 0");
CHECK(g_tap_cb_idx == 0, "tap cb idx0 got %d", g_tap_cb_idx);
/* 点 row1,col1(idx=4) → 该格命中 */
int idx4 = meow_lazygrid_hit(g, GX + 1 * (CW + GAP) + 10, GY + ROWH + 10);
CHECK(idx4 == 4, "hit row1col1 idx=%d (expect 4)", idx4);
meow_lazygrid_free(g);
}
/* ============ T2 默认 renderer:卡片底 / 标题 / 选中高亮 / 禁用 ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
g_count_override = 3; g_disabled_at = 2; /* 3 格:cell2 禁用 */
meow_ui_clear(th->bg);
meow_lazygrid_set_sel(g, 0);
meow_lazygrid_draw(g, th);
/* 选中 cell0 → primaryContainer 底([GX,GY,100,80]) */
CHECK(region_has(GX + 4, GY + 4, 90, 60, pc_r, pc_g, pc_b) > 40, "selected cell primaryContainer bg");
/* 未选中 cell1 → surfaceContainer 底(MD3 容器:格内对称内缩 m=8 的圆角卡) */
int x1 = GX + 1 * (CW + GAP);
CHECK(region_has(x1 + 4, GY + 4, 90, 60, sc_r, sc_g, sc_b) > 40, "cell1 surfaceContainer bg");
/* 标题画在 cell0 卡内底部(选中 → onPrimaryContainer;副标题在下方 onPrimaryContainer 同色带)。
* 镜像 lg_draw_cell 的 ty:cy=GY+m、chh=CH-2m、pad=8 → ty = GY+CH-m-pad-thh-(thh+2)。 */
int thh = meow_ui_text_height(th->font_body);
int title_y = GY + CH - 8 - 8 - thh - (thh + 2);
int c0cx = GX + CW / 2;
CHECK(region_has(c0cx - 30, title_y, 60, thh, opc_r, opc_g, opc_b) > 2, "cell0 title drawn (onPrimaryContainer)");
/* 禁用 cell2 变淡:卡内左上角(图标/文本区之外)不再是纯 surfaceContainer(被 onSurface α 叠暗) */
int x2 = GX + 2 * (CW + GAP);
CHECK(!px_near(x2 + 8 + 4, GY + 8 + 4, sc_r, sc_g, sc_b), "disabled cell overlaid (bg dimmed)");
meow_lazygrid_free(g);
}
/* ============ T3 完全自绘(set_item_draw) ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
g_count_override = 3; g_disabled_at = -1; g_draw_total = 0;
CHECK(meow_lazygrid_set_item_draw(g, mydraw, NULL) == 0, "set_item_draw");
meow_ui_clear(th->bg);
meow_lazygrid_draw(g, th);
CHECK(g_draw_total == 3, "draw_cb per visible cell got %d (expect 3)", g_draw_total);
CHECK(region_has(GX + 4, GY + 4, 8, 8, 0xFF, 0x00, 0x00) > 0, "custom cell content drawn");
meow_lazygrid_free(g);
}
/* ============ T4 空态 ============ */
{
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
g_count_override = 0;
meow_lazygrid_set_empty_text(g, "No data");
const meow_theme *t2 = meow_theme_get();
meow_ui_clear(th->bg);
meow_lazygrid_draw(g, t2);
int thh = meow_ui_text_height(t2->font_body);
int ety = GY + (GH - thh) / 2;
CHECK(region_has(GX + GW / 2 - 60, ety, 120, thh, osv_r, osv_g, osv_b) > 2, "empty state text drawn");
meow_lazygrid_free(g);
}
/* ============ T5 meow_scroll 集成(fling/snap) ============ */
{
g_count_override = -1; g_disabled_at = -1;
meow_lazygrid *g = meow_lazygrid_create(GX, GY, GW, GH, COLS, CW, CH, GAP);
meow_lazygrid_set_vsource(g, lg_count, lg_bind, NULL);
meow_scroll *s = meow_lazygrid_scroll_attach(g);
CHECK(s != NULL, "scroll attach");
meow_scroll_set_snap(s, ROWH);
uint64_t t = meow_ui_time_ms();
for (int i = 0; i < 12; i++) meow_scroll_track(s, 30, t + (uint64_t)i * 16);
int start = meow_scroll_offset(s);
CHECK(start > 0, "drag advances offset got %d", start);
meow_scroll_fling(s);
CHECK(meow_scroll_active(s) == 1, "fling active");
int max_off = start, guard = 0;
while (meow_scroll_active(s) && guard++ < 300) {
meow_anim_tick();
int cur = meow_scroll_offset(s);
if (max_off < cur) max_off = cur;
usleep(10000);
}
meow_anim_tick();
int end = meow_scroll_offset(s);
CHECK(end >= start, "fling advances %d vs %d", end, start);
/* snap 到行边界;若撞底(max_scroll=420 非 row_h 的整数倍)则停在边界(clamp 优先) */
CHECK((end % ROWH) == 0 || end == meow_lazygrid_max_scroll(g),
"fling ends snapped-or-boundary got %d (%%%d, max %d)", end, end % ROWH, meow_lazygrid_max_scroll(g));
CHECK(end <= meow_lazygrid_max_scroll(g), "fling in range %d (max %d)", end, meow_lazygrid_max_scroll(g));
CHECK(meow_lazygrid_scroll(g) == end, "grid scroll mirrors offset");
meow_scroll_free(s);
meow_lazygrid_free(g);
}
meow_ui_shutdown();
printf("lazygrid: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+2
View File
@@ -206,6 +206,7 @@ int main(void)
/* 双行(lines=2,默认):行 0 title+subtitle + icon(FOLDER) + 尾件(ARROW) */
int lvx = 20, lvy = 50, lvw = 440, lvh = 120;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,本测试专测扁平虚拟化渲染路径 */
CHECK(meow_listview_set_item_h(lv, itemh) == itemh, "item_h");
CHECK(meow_listview_set_vsource(lv, vcount, vbind, NULL) == 0, "vsource");
CHECK(meow_listview_set_item_renderer(lv, 2, 1, 1) == 0, "renderer");
@@ -267,6 +268,7 @@ int main(void)
int lvx = 20, lvy = 50, lvw = 440, lvh = 120;
g_count_override = 3; g_section_at = 1;
meow_widget *lv = meow_listview_create(lvx, lvy, lvw, lvh, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,段头 band 断言按扁平 */
meow_listview_set_item_h(lv, itemh);
meow_listview_set_vsource(lv, vcount, vbind, NULL);
meow_ui_clear(th->bg);
+178
View File
@@ -0,0 +1,178 @@
/* test_meow_ui_select.c — 宿主验证枚举/预设选择器(meow_select):
* T1 打开→点选项→单选高亮(primaryContainer)+ 选中 + 关闭即回调(on_change(idx))+ 返回值。
* T2 取消:点面板外(遮罩)→ 返回 -1,不调 on_change。
* T3 meow_select_show 一次性:返回选中下标。
*
* 阻塞式模态用后台线程驱动(同 test_ime_fix 范式),主线程经 meow_input_inject 喂触摸。
* 编译(宿主 gcc):
* gcc -o test_meow_ui_select test_meow_ui_select.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_select
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_input_inject + back_buf 读 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
/* ---- 驱动:后台线程跑阻塞 meow_select_open ---- */
typedef struct {
meow_select *s;
int ret;
} sel_arg;
static void *sel_driver(void *p)
{
sel_arg *a = (sel_arg *)p;
a->ret = meow_select_open(a->s);
return NULL;
}
/* ---- on_change 回调记录 ---- */
static int g_cb = -1;
static void on_change(int idx, void *ctx) { (void)ctx; g_cb = idx; }
/* 计算面板/选项列表几何(与 meow_select_open 同式;用于注入命中坐标 + 像素采样) */
static int sel_list_y(int nitems)
{
const meow_theme *th = meow_theme_get();
int W = meow_ui_width(), H = meow_ui_height();
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
int pad = 24, rowh = 48;
int title_h = meow_ui_text_height(th->font_title);
int avail = H - sb - 20;
int min_h = pad + title_h + 12 + rowh + 20;
int want = min_h;
int est = pad + title_h + 12 + nitems * rowh + 20;
if (est > want) want = est;
if (want > avail) want = avail;
if (want < min_h) want = (min_h < avail) ? min_h : avail;
int py = H - want;
return py + pad + title_h + 12;
}
static int sel_sheet_top(int nitems)
{
const meow_theme *th = meow_theme_get();
int H = meow_ui_height();
int sb = meow_ui_statusbar_height(); if (sb < 0) sb = 0;
int pad = 24, rowh = 48;
int title_h = meow_ui_text_height(th->font_title);
int avail = H - sb - 20;
int min_h = pad + title_h + 12 + rowh + 20;
int want = min_h;
int est = pad + title_h + 12 + nitems * rowh + 20;
if (est > want) want = est;
if (want > avail) want = avail;
if (want < min_h) want = (min_h < avail) ? min_h : avail;
return H - want;
}
#define SEL_ROW 48
/* ============ T1 选中 + 回调 + 单选高亮 ============ */
static void test_select_highlight(void)
{
meow_select *s = meow_select_create("Choose", 2);
meow_select_add_item(s, "Alpha");
meow_select_add_item(s, "Beta");
meow_select_add_item(s, "Gamma");
meow_select_set_on_change(s, on_change, NULL);
int list_y = sel_list_y(3);
sel_arg a = { s, -999 };
g_cb = -1;
pthread_t th; pthread_create(&th, NULL, sel_driver, &a);
/* 等 open 的 drain + 首帧绘制完成(高亮已画进 back_buf) */
usleep(150000);
/* 单选高亮:cur=2(第 3 行)选中背景 = primaryContainer */
const meow_theme *thm = meow_theme_get();
int pc_r, pc_g, pc_b; UNPK(thm->primaryContainer, pc_r, pc_g, pc_b);
int hx = 6, hy = list_y + 2 * SEL_ROW + SEL_ROW / 2;
int r, g, b; rd_px(hx, hy, &r, &g, &b);
CHECK(near2(r, pc_r) && near2(g, pc_g) && near2(b, pc_b),
"T1 highlight row2 primaryContainer got %02X%02X%02X", r, g, b);
/* 点第 1 行(idx 0)→ 选中 + 关闭 + 回调 */
meow_input_inject(MEOW_EV_TAP, 100, list_y + 0 * SEL_ROW + SEL_ROW / 2);
pthread_join(th, NULL);
CHECK(a.ret == 0, "T1 open return idx0 got %d", a.ret);
CHECK(g_cb == 0, "T1 on_change(0) got %d", g_cb);
meow_select_free(s);
}
/* ============ T2 取消(点面板外) ============ */
static void test_select_cancel(void)
{
meow_select *s = meow_select_create("Choose", 0);
meow_select_add_item(s, "Alpha");
meow_select_add_item(s, "Beta");
meow_select_set_on_change(s, on_change, NULL);
sel_arg a = { s, -999 };
g_cb = -1;
pthread_t th; pthread_create(&th, NULL, sel_driver, &a);
usleep(150000);
int top = sel_sheet_top(2);
meow_input_inject(MEOW_EV_TAP, 240, (top > 30) ? 10 : 10); /* 面板外(遮罩) */
pthread_join(th, NULL);
CHECK(a.ret == -1, "T2 cancel returns -1 got %d", a.ret);
CHECK(g_cb == -1, "T2 on_change NOT fired on cancel got %d", g_cb);
meow_select_free(s);
}
/* ============ T3 meow_select_show 一次性 ============ */
static void *show_driver(void *p)
{
(void)p;
static const char *opts[] = { "A", "B", "C", "D" };
static int r = -999;
r = meow_select_show("Preset", opts, 4, 1);
return (void *)(long)r;
}
static void test_select_show(void)
{
g_cb = -1;
pthread_t th; pthread_create(&th, NULL, show_driver, NULL);
usleep(150000);
int list_y = sel_list_y(4);
meow_input_inject(MEOW_EV_TAP, 100, list_y + 2 * SEL_ROW + SEL_ROW / 2); /* 选第 3 项 idx2 */
long r = 0; pthread_join(th, (void **)&r);
CHECK(r == 2, "T3 show returns idx2 got %ld", r);
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
test_select_highlight();
test_select_cancel();
test_select_show();
meow_ui_shutdown();
printf("select: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+210
View File
@@ -0,0 +1,210 @@
/* test_meow_ui_swipe.c — 宿主验证列表行左滑删除(§5.5,M7.3 缺口 #9):
* T1 滑动手势判定与冲突消解:begin 命中可滑行返回 1;横向小位移 = swipe(move 返回 1 消费);
* 纵向大位移 = 滚动手势优先(move 返回 0 并取消本行 swipe);非启用行 begin 返回 0。
* T2 左滑超阈值 → 删除动画(meow_anim 滑出),结束后 on_delete 回调恰一次 + swipe 状态复位。
* T3 未超阈值 → 回弹,on_delete 不触发,swipe_dx 回到 0。
* T4 点按(无位移):begin 后 end(未 started)返回 0(App 走 tap_at),不触发 on_delete。
* T5 右露删除区像素:滑动中重绘列表 → 右露区画主题 error 底 + 行内容左移。
*
* 编译(宿主 gcc):
* gcc -o test_meow_ui_swipe test_meow_ui_swipe.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_swipe
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static void rd_px(int x, int y, int *r, int *g, int *b)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch(), bpp = meow_ui_back_buf_bpp();
if (!bb || x < 0 || y < 0) { *r = *g = *b = 0; return; }
if (bpp == 16) {
uint16_t px = *(uint16_t *)(bb + (size_t)y * line + (size_t)x * 2);
*r = (((px >> 11) & 0x1F) << 3) | ((px >> 11) & 0x1F) >> 2;
*g = (((px >> 5) & 0x3F) << 2) | ((px >> 5) & 0x3F) >> 4;
*b = ((px & 0x1F) << 3) | ((px & 0x1F) >> 2);
} else {
uint32_t px = *(uint32_t *)(bb + (size_t)y * line + (size_t)x * 4);
*r = (px >> 16) & 0xFF; *g = (px >> 8) & 0xFF; *b = px & 0xFF;
}
}
static int near2(int a, int b) { int d = a - b; if (d < 0) d = -d; return d <= 14; }
static int px_near(int x, int y, int r, int g, int b)
{
int rr, gg, bb; rd_px(x, y, &rr, &gg, &bb);
return near2(rr, r) && near2(gg, g) && near2(bb, b);
}
static int region_has(int x, int y, int w, int h, int r, int g, int b)
{
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px_near(x + dx, y + dy, r, g, b)) cnt++;
return cnt;
}
#define UNPK(c, rr, gg, bb) do { (rr) = ((c) >> 16) & 0xFF; (gg) = ((c) >> 8) & 0xFF; (bb) = (c) & 0xFF; } while (0)
#define LVX 20
#define LVY 50
#define LVW 440
#define LVH 300
#define ITEM 56
#define CNT 10
static int del_count; /* on_delete 回调次数 */
static int del_idx; /* 最后一次删除 idx */
static void on_delete(int idx, void *ctx) { (void)ctx; del_count++; del_idx = idx; }
static int scount(void *ctx) { (void)ctx; return CNT; }
static void sbind(int idx, meow_lv_item *it, void *ctx)
{
(void)ctx;
memset(it, 0, sizeof *it);
static char t[32];
snprintf(t, sizeof t, "notif %d", idx);
it->title = t; it->icon_kind = MEOW_LIST_ICON_NONE; it->right = MEOW_LIST_RIGHT_NONE;
it->enabled = 1;
}
static meow_widget *make_list(void)
{
meow_widget *lv = meow_listview_create(LVX, LVY, LVW, LVH, 0, NULL, NULL, MEOW_LIST_SEL_NONE);
meow_listview_set_item_h(lv, ITEM);
meow_listview_set_vsource(lv, scount, sbind, NULL);
meow_listview_set_item_renderer(lv, 2, 0, 0); /* 双行、无图标、无尾件 */
meow_listview_set_swipe_delete(lv, 1, on_delete, NULL);
return lv;
}
/* 推进 meow_anim 直到 swipe 动画结束(模拟 on_tick 节拍)。 */
static void pump_swipe(meow_widget *lv)
{
int guard = 0;
while (meow_listview_swipe_active(lv) && guard++ < 80) {
meow_anim_tick();
usleep(10000);
}
meow_anim_tick();
}
int main(void)
{
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
const meow_theme *th = meow_theme_get();
int err_r, err_g, err_b; UNPK(th->error, err_r, err_g, err_b);
int row0y = LVY + ITEM / 2; /* 行 0 中心 y(scroll=0) */
/* ============ T1 手势判定 / 冲突消解 ============ */
{
meow_widget *lv = make_list();
/* begin 命中可滑行行 → 1 */
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T1 begin on swipe row = 1");
/* 横向小位移 → 裁定 swipe,move 返回 1(消费) */
CHECK(meow_listview_swipe_move(lv, 40, row0y) == 1, "T1 move horizontal = 1 (consumed)");
int dx = meow_listview_swipe_offset(lv);
CHECK(dx == -60, "T1 dx=-60 got %d", dx);
/* 又一小位移(不超容差也不改变方向) */
CHECK(meow_listview_swipe_move(lv, 30, row0y) == 1, "T1 move horizontal 2 = 1");
CHECK(meow_listview_swipe_end(lv, 30, row0y) == 1, "T1 end started swipe = 1 (bounce)");
pump_swipe(lv);
CHECK(meow_listview_swipe_offset(lv) == 0, "T1 bounce back dx=0 got %d", meow_listview_swipe_offset(lv));
CHECK(del_count == 0, "T1 no delete on bounce");
meow_widget_free(lv);
}
{
/* 纵向大位移 → 滚动手势优先(move 返回 0 + 取消) */
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T1 begin2 = 1");
CHECK(meow_listview_swipe_move(lv, 100, row0y + 80) == 0, "T1 vertical move = 0 (yield to scroll)");
CHECK(meow_listview_swipe_active(lv) == 0, "T1 vertical cancels swipe");
CHECK(meow_listview_swipe_offset(lv) == 0, "T1 vertical dx=0");
meow_widget_free(lv);
}
{
/* 非启用行 begin → 0 */
meow_widget *lv = make_list();
meow_listview_set_swipe_delete(lv, 0, NULL, NULL);
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 0, "T1 disabled begin = 0");
meow_widget_free(lv);
}
/* ============ T2 超阈值 → 删除动画 + on_delete ============ */
{
del_count = 0; del_idx = -1;
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T2 begin = 1");
/* 左滑 200px(阈值=LVW*2/5=176)→ 超阈值 */
CHECK(meow_listview_swipe_move(lv, 100 - 200, row0y) == 1, "T2 move big left = 1");
CHECK(meow_listview_swipe_end(lv, 100 - 200, row0y) == 1, "T2 end = 1 (delete anim)");
CHECK(meow_listview_swipe_active(lv) == 1, "T2 delete anim active");
pump_swipe(lv);
CHECK(del_count == 1, "T2 on_delete fired once got %d", del_count);
CHECK(del_idx == 0, "T2 on_delete idx0 got %d", del_idx);
CHECK(meow_listview_swipe_active(lv) == 0, "T2 state reset inactive");
CHECK(meow_listview_swipe_offset(lv) == 0, "T2 dx reset 0 got %d", meow_listview_swipe_offset(lv));
meow_widget_free(lv);
}
/* ============ T3 未超阈值 → 回弹,on_delete 不触发 ============ */
{
del_count = 0;
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T3 begin = 1");
CHECK(meow_listview_swipe_move(lv, 55, row0y) == 1, "T3 move left 45px = 1");
CHECK(meow_listview_swipe_offset(lv) == -45, "T3 dx=-45 got %d", meow_listview_swipe_offset(lv));
CHECK(meow_listview_swipe_end(lv, 55, row0y) == 1, "T3 end = 1 (bounce anim)");
CHECK(meow_listview_swipe_active(lv) == 1, "T3 bounce active");
pump_swipe(lv);
CHECK(del_count == 0, "T3 on_delete NOT fired got %d", del_count);
CHECK(meow_listview_swipe_offset(lv) == 0, "T3 bounce dx=0 got %d", meow_listview_swipe_offset(lv));
CHECK(meow_listview_swipe_active(lv) == 0, "T3 inactive after bounce");
meow_widget_free(lv);
}
/* ============ T4 点按(无位移)= 行点击,end 返回 0 ============ */
{
del_count = 0;
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T4 begin = 1");
CHECK(meow_listview_swipe_end(lv, 100, row0y) == 0, "T4 tap end = 0 (let App tap_at)");
CHECK(del_count == 0, "T4 no delete on tap");
CHECK(meow_listview_swipe_active(lv) == 0, "T4 inactive after tap");
meow_widget_free(lv);
}
/* ============ T5 右露删除区像素(默认 renderer) ============ */
{
meow_widget *lv = make_list();
CHECK(meow_listview_swipe_begin(lv, 100, row0y) == 1, "T5 begin = 1");
CHECK(meow_listview_swipe_move(lv, 100 - 200, row0y) == 1, "T5 move big left = 1");
meow_ui_clear(th->bg);
meow_widget_draw(lv);
/* 右露区 [LVX+LVW+dx, row0, -dx, ITEM] = [260,50,200,56] → error 底 */
CHECK(region_has(270, row0y - ITEM / 2 + 4, 180, ITEM - 8, err_r, err_g, err_b) > 50,
"T5 delete reveal error bg got %d", region_has(270, row0y - ITEM / 2 + 4, 180, ITEM - 8, err_r, err_g, err_b));
meow_listview_swipe_end(lv, 100 - 200, row0y);
pump_swipe(lv);
meow_widget_free(lv);
}
meow_ui_shutdown();
printf("swipe: %d pass / %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+122
View File
@@ -0,0 +1,122 @@
/* test_meow_ui_theme_export.c — M7.0a 主题收口前置(缺口#6 SDK 半)宿主验证:
* §8.1.5 色板导出/注入——meow_theme_export 拷当前权威主题;meow_theme_inject 覆写当前生效主题。
*
* 覆盖:
* T1 导出一致性:export 后关键字段 == meow_theme_get()(当前 style×mode 权威板);
* T2 注入:inject 一份自定义板 → meow_theme_get()/export 都返回注入板(单一权威反映);
* 注入恢复内建:inject(NULL) 后回到目标 style×mode 的内建板;
* T3 launcher 薄壳映射契约:导出板能映射出 meow_ui_theme_t 的 9 个角色字段(surface/
* surface_variant/surface_container/primary/on_surface/on_surface_variant/outline/error/
* surface_tint),值与 SDK meow_theme 对应字段一致。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_ui_theme_export test_meow_ui_theme_export.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_theme_export
*/
#include "meow_ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* launcher meow_ui_theme_t 的 9 字段形状(测试内复刻字段名,不依赖 launcher 内部头)。 */
typedef struct {
uint32_t surface, surface_variant, surface_container;
uint32_t primary, on_surface, on_surface_variant;
uint32_t outline, error, surface_tint;
} shell_theme;
static shell_theme export_shell(const meow_theme *t)
{
shell_theme s;
s.surface = t->surface;
s.surface_variant = t->surfaceVariant;
s.surface_container = t->surfaceContainer;
s.primary = t->primary;
s.on_surface = t->onSurface;
s.on_surface_variant= t->onSurfaceVariant;
s.outline = t->outline;
s.error = t->error;
s.surface_tint = t->surfaceTint;
return s;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/theme_export.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
/* ---- T1 导出一致性(MD3 dark) ---- */
printf("T1 export consistency\n");
meow_theme_set_mode(MEOW_THEME_DARK);
meow_ui_set_style(MEOW_STYLE_MD3);
const meow_theme *t0 = meow_theme_get();
meow_theme e0; meow_theme_export(&e0);
CHECK(e0.surface == t0->surface, "export.surface == get.surface");
CHECK(e0.primary == t0->primary, "export.primary == get.primary");
CHECK(e0.surfaceVariant== t0->surfaceVariant,"export.surfaceVariant == get.surfaceVariant");
CHECK(e0.onSurface == t0->onSurface, "export.onSurface == get.onSurface");
CHECK(e0.radius == t0->radius, "export.radius == get.radius");
CHECK(e0.bg == t0->bg, "export.bg == get.bg (background override applied)");
/* MIUIx light:确认导出随 style×mode 切换(不是固定板) */
meow_ui_set_style(MEOW_STYLE_MIUIX);
meow_theme_set_mode(MEOW_THEME_LIGHT);
const meow_theme *t1 = meow_theme_get();
meow_theme e1; meow_theme_export(&e1);
CHECK(e1.primary == 0xF57200 && t1->primary == 0xF57200, "MIUIx light primary exported");
CHECK(e1.bg == t1->bg && e1.surface == t1->surface, "MIUIx light bg/surface exported");
CHECK(e1.surface == (uint32_t)0xF7F3EE, "MIUIx light surface == warm default");
/* ---- T2 注入覆写 + 恢复内建 ---- */
printf("T2 inject override + restore\n");
meow_theme custom; memset(&custom, 0, sizeof custom);
custom.surface = 0x112233; custom.primary = 0xABCDEF; custom.onSurface = 0x0F0F0F;
custom.radius = 99; custom.radius_card = 100;
meow_theme_inject(&custom);
const meow_theme *ti = meow_theme_get();
meow_theme ei; meow_theme_export(&ei);
CHECK(ti->primary == 0xABCDEF && ti->surface == 0x112233 && ti->radius == 99,
"inject -> get() reads injected palette");
CHECK(ei.primary == 0xABCDEF && ei.surface == 0x112233, "inject -> export reads injected palette");
/* meow_theme_export == meow_theme_get even when injected(单一权威) */
CHECK(ei.onSurface == ti->onSurface && ei.radius_card == ti->radius_card,
"export == get while injected");
/* 注入恢复内建 */
meow_theme_inject(NULL);
const meow_theme *tr = meow_theme_get();
CHECK(tr->primary == 0xF57200 && tr->surface == 0xF7F3EE,
"inject(NULL) restores builtin (MIUIx light)");
/* ---- T3 launcher 薄壳映射契约(9 角色字段) ---- */
printf("T3 shell mapping contract\n");
meow_theme_export(&e1);
shell_theme sh = export_shell(&e1);
CHECK(sh.surface == e1.surface, "shell.surface == surface");
CHECK(sh.surface_variant == e1.surfaceVariant, "shell.surface_variant == surfaceVariant");
CHECK(sh.surface_container == e1.surfaceContainer, "shell.surface_container == surfaceContainer");
CHECK(sh.primary == e1.primary, "shell.primary == primary");
CHECK(sh.on_surface == e1.onSurface, "shell.on_surface == onSurface");
CHECK(sh.on_surface_variant == e1.onSurfaceVariant, "shell.on_surface_variant == onSurfaceVariant");
CHECK(sh.outline == e1.outline, "shell.outline == outline");
CHECK(sh.error == e1.error, "shell.error == error");
CHECK(sh.surface_tint == e1.surfaceTint, "shell.surface_tint == surfaceTint");
/* 关键角色字段值与 MIUIx light 内建一致(证明读的是 SDK 单一权威,而非 launcher 自持 4 套) */
CHECK(sh.surface == 0xF7F3EE && sh.primary == 0xF57200 && sh.error == 0xB3261E,
"shell roles match SDK builtin (single authority)");
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+152
View File
@@ -0,0 +1,152 @@
/* test_meow_ui_view_event.c — M7.0a 缺口#7 宿主验证:meow_view 事件接线 + 递归 enter/leave。
*
* A 部(无 run 循环,直调 meow_view_*):递归 enter/leave 顺序 + 运行期动态挂子视图立即进入
* (view.c meow_view_add_child 修正)+ 子>父 on_event 路由。
* B 部(经 meow_ui_run 主循环):把语义事件注入队列 → run 循环先经 meow_view_dispatch_event
* 路由,命中的事件不再落 App on_* 回调(on_tap/on_key/on_swipe/long_press 零触发),未命中
* 的事件照常回落 App 回调——验证「接线于 meow_ui_run、于 §6.4 分流之上」。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_meow_ui_view_event test_meow_ui_view_event.c test_host_stub.c \
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_meow_ui_view_event
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* MEOW_EV_* */
#include "meow_ui_view.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <assert.h>
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) printf(" PASS %s\n", name); \
else { printf(" FAIL %s\n", name); g_fail++; } } while (0)
/* ---------- A 部:递归 enter/leave 顺序 + 运行期动态挂子 ---------- */
static int g_trace[16]; static int g_trace_n = 0;
static void vtrace_enter(meow_view *v, void *ctx) { (void)ctx; g_trace[g_trace_n++] = v->id; }
static void vtrace_leave(meow_view *v, void *ctx) { (void)ctx; g_trace[g_trace_n++] = -v->id; }
static int g_child3_hits = 0;
static int vev_child3(meow_view *v, int ev, int a, int b, void *ctx)
{ (void)v;(void)ctx; if (ev == MEOW_EV_TAP && a == 33 && b == 33) { g_child3_hits++; return 1; } return 0; }
/* ---------- B 部:run 循环接线 ---------- */
static int g_tap_calls = 0, g_key_calls = 0, g_swipe_calls = 0, g_lp_calls = 0;
static void app_on_tap(int x, int y) { (void)x;(void)y; g_tap_calls++; }
static void app_on_key(int k, int d) { (void)k;(void)d; g_key_calls++; }
static void app_on_swipe(int dir, int dx, int dy) { (void)dir;(void)dx;(void)dy; g_swipe_calls++; }
static void app_lp(int x, int y) { (void)x;(void)y; g_lp_calls++; }
/* 父 view:消费 TAP@(10,10) 与 LONG_PRESS(任何)。 */
static int vev_parent(meow_view *v, int ev, int a, int b, void *ctx)
{ (void)v;(void)ctx;
if (ev == MEOW_EV_TAP && a == 10 && b == 10) return 1;
if (ev == MEOW_EV_LONG_PRESS) return 1;
return 0; }
/* 子 view:消费 KEY(任何)与 SWIPE_LEFT。 */
static int vev_child(meow_view *v, int ev, int a, int b, void *ctx)
{ (void)v;(void)b;(void)ctx;
if (ev == MEOW_EV_KEY) return 1;
if (ev == MEOW_EV_SWIPE && a == MEOW_SWIPE_LEFT) return 1;
return 0; }
static void *driver(void *arg)
{
(void)arg;
usleep(700000); /* 等 startup cooldown(500ms) 过后再发 */
meow_input_inject(MEOW_EV_KEY, MEOW_KEY_VOLUMEUP, 1); /* child 消费 → app_on_key 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_TAP, 10, 10); /* parent 消费 → app_on_tap 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_TAP, 50, 50); /* 无 view 命中 → 回落 app_on_tap */
usleep(80000);
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80); /* child 消费 → app_on_swipe 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_RIGHT, 80); /* 无 view 命中 → 回落 app_on_swipe */
usleep(80000);
meow_input_inject(MEOW_EV_LONG_PRESS, 10, 10); /* parent 消费 → long_press 不触发 */
usleep(80000);
meow_ui_request_exit();
return NULL;
}
int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/view_event.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
/* ================= A 部:递归 enter/leave + 动态挂子 ================= */
printf("A: recursive enter/leave + runtime child add\n");
{
static const meow_view_hooks ph = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
.on_event = vev_parent };
static const meow_view_hooks ch = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
.on_event = vev_child };
static const meow_view_hooks c3 = { .on_enter = vtrace_enter, .on_leave = vtrace_leave,
.on_event = vev_child3 };
meow_view *pv = meow_view_create(1, MEOW_VIEW_ACTIVITY, &ph, NULL);
meow_view *cv = meow_view_create(2, MEOW_VIEW_CONTAINER, &ch, NULL);
assert(meow_view_add_child(pv, cv) == 0);
g_trace_n = 0;
meow_view_page_push(1); /* pv.enter → cv.enter */
CHECK(g_trace_n == 2 && g_trace[0] == 1 && g_trace[1] == 2, "push: parent.enter -> child.enter");
/* 运行期动态挂第三个子视图(父已 ENTERED)→ 立即进入(view.c 修正) */
meow_view *c3v = meow_view_create(3, MEOW_VIEW_CONTAINER, &c3, NULL);
g_trace_n = 0;
assert(meow_view_add_child(pv, c3v) == 0);
CHECK(g_trace_n == 1 && g_trace[0] == 3, "runtime add-child-while-entered -> child.enter fires");
/* 新挂子 view 参与事件路由:命中 (33,33) 被子 3 消费 */
g_child3_hits = 0;
CHECK(meow_view_dispatch_event(MEOW_EV_TAP, 33, 33) == 1 && g_child3_hits == 1,
"runtime-added child participates in dispatch");
/* 离开:子视图(挂载序 2 → 3)先后离开 → 父(递归离开,子->父) */
g_trace_n = 0;
meow_view_page_pop();
CHECK(g_trace_n == 3 && g_trace[0] == -2 && g_trace[1] == -3 && g_trace[2] == -1,
"pop: child2.leave -> child3.leave -> parent.leave");
meow_view_free(pv); /* 递归释放 2/3 */
printf(" PASS A: recursive order + dynamic child\n");
}
/* ================= B 部:meow_ui_run 接线(§6.4 之上) ================= */
printf("B: meow_view_dispatch_event wired into meow_ui_run\n");
{
static const meow_view_hooks ph = { .on_event = vev_parent };
static const meow_view_hooks ch = { .on_event = vev_child };
meow_view *pv = meow_view_create(20, MEOW_VIEW_ACTIVITY, &ph, NULL);
meow_view *cv = meow_view_create(21, MEOW_VIEW_CONTAINER, &ch, NULL);
assert(meow_view_add_child(pv, cv) == 0);
meow_view_page_push(20); /* 进入父+子(g_top_view=20) */
g_tap_calls = g_key_calls = g_swipe_calls = g_lp_calls = 0;
meow_ui_set_long_press_cb(app_lp);
meow_ui_events ev; memset(&ev, 0, sizeof ev);
ev.on_tap = app_on_tap; ev.on_key = app_on_key; ev.on_swipe = app_on_swipe;
pthread_t thd; pthread_create(&thd, NULL, driver, NULL);
meow_ui_run(&ev); /* 阻塞直到 driver 里 request_exit */
pthread_join(thd, NULL);
CHECK(g_key_calls == 0, "KEY consumed by child -> app_on_key NOT called");
CHECK(g_tap_calls == 1, "TAP(10,10) consumed by parent; TAP(50,50) fell through -> exactly 1 on_tap");
CHECK(g_swipe_calls == 1, "SWIPE_LEFT consumed by child; SWIPE_RIGHT fell through -> exactly 1 on_swipe");
CHECK(g_lp_calls == 0, "LONG_PRESS consumed by parent -> long_press cb NOT called");
printf(" PASS B: run loop routes through views, fallback preserved\n");
meow_view_page_pop();
meow_view_free(pv);
}
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
+1
View File
@@ -318,6 +318,7 @@ int main(void)
* lv 位于 (20,240,320,112)item_h=56 → 行0 y240..296、行1 296..352。
* 断言全部「画完读 back_buf 像素」(region_has/px_near),非目检。 */
meow_widget *lv = W(meow_listview_create(20, 240, 320, 112, 4, lv_src, NULL, MEOW_LIST_SEL_SINGLE));
meow_listview_set_container(lv, 0); /* 锁扁平基线:SDK 默认已变 MD3 容器,此测试专测 set_container(0) 扁平回退路径 */
assert(meow_listview_set_item_h(lv, 56) == 56);
const meow_theme *lth = meow_theme_get();
int lvx = 20, lvy = 240, lvw = 320, lvh = 112, itemh = 56;
+27
View File
@@ -0,0 +1,27 @@
/* test_net_ipc.c — MeowOS SDK 网络自愿通道 宿主自测(非生产代码)。
*
* 通过链接 meow_net_ipc.c 调 meow_net_policy_check()/meow_net_report(),对运行中的 netmgrd
* (MEOW_NET_SOCK 指向测试 socket)验证:
* - meow_net_policy_check(host):按 MEOW_LOG_APP 实体判定 allow/block(netmgrd 决策权威);
* - meow_net_report(rx,tx):best-effort 上报(netmgrd 记入 entity_bytes)。
* 用法:cc -Iinclude -I. -I../meow_launcherd test_net_ipc.c meow_net_ipc.c -o test_net_ipc
* 然后 MEOW_NET_SOCK=<sock> MEOW_LOG_APP=<pkg> ./test_net_ipc
*/
#include "include/meow_ui_net.h"
#include <stdio.h>
int main(void)
{
/* CHECK:域名放行/拒绝判定(netmgrd 决策)。 */
int a = meow_net_policy_check("music.163.com");
printf("CHECK_GOOD=%d\n", a); /* 期望 0(allow/放行) */
int b = meow_net_policy_check("ads.example.com");
printf("CHECK_BLOCK=%d\n", b); /* 期望 1(block/拒绝,若策略 deny 该域) */
/* 空 host → 0 放行(防御)。 */
int c = meow_net_policy_check(NULL);
printf("CHECK_NULL=%d\n", c);
/* REPORT:best-effort 上报。 */
meow_net_report(12345, 67890);
printf("REPORT_DONE\n");
return 0;
}
+49 -21
View File
@@ -1,14 +1,19 @@
/* test_overlay_suppress.c — §10.5 叠加让位 present 自抑宿主验证(离屏 480x720,16bpp)。
*
* 背景:媒体播放中的 App 前台唤起通知中心时 launcher 不 SIGSTOP(保 PCM 供流),改由 SDK
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏。
* present 层见 /tmp/meow_notif_active(宿主用 MEOW_NOTIF_ACTIVE_FILE 覆写)自抑上屏;
* 批①后台常驻后标记**内容化**:内容=当前前台 app_id(空=菜单/叠加=全抑制)。本测试同时验证
* 批①多 App 常驻的「前台/后台」判定:内容==本进程 app_id(env MEOW_LOG_APP)→ 前台不抑制;
* 内容为其它 app_id / 空 → 后台常驻/被覆盖 → 自抑。
*
* 覆盖:
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
* T2 有标记:flush 被抑制(只画进 back_buf 不 present:frames 不变、抑制计数递增);
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
* T3 标记消失:下一次 flush 恢复上屏(抑制期累积脏区一次上屏,无需重画);
* T4 系统 UI 进程(launcher,system_mode=1)即使有标记也不抑制(面板必须照常上屏)。
* T1 无标记:flush 正常上屏(frames_presented 递增,抑制计数 0);
* T2 标记=其它 app_id(后台常驻)→ 抑制(只画进 back_buf 不上屏:frames 不变、计数递增);
* T2b 抑制期第二次 flush 仍被抑制(200ms 节流缓存内行为一致);
* T2c 标记=自身 app_id(前台 App)→ 不抑制(恢复上屏,恢复边沿整帧重画;计数不再增长);
* T3 标记=空内容(菜单/叠加全抑制)→ 仍抑制;
* T3b 标记消失 → 恢复上屏(抑制期累积脏区一次上屏,无需重画);
* T4 系统 UI 进程(launcher,system_mode=1)即使有标记也不抑制(面板必须照常上屏)。
*
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime + 打桩 font/global):
* gcc -Wall -Wextra -o test_overlay_suppress test_overlay_suppress.c test_host_stub.c \
@@ -26,13 +31,16 @@
#include <unistd.h>
#include <fcntl.h>
#define MARKER "/tmp/test_notif_active_sdk"
#define MARKER "/tmp/test_notif_active_sdk"
#define SELF_ID "testapp"
static void marker_write(void)
static void marker_write(const char *content)
{
int fd = open(MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
assert(fd >= 0);
if (write(fd, "1", 1) < 0) assert(0);
if (content && content[0]) {
if (write(fd, content, strlen(content)) < 0) assert(0);
}
close(fd);
}
@@ -40,6 +48,7 @@ int main(void)
{
setenv("MEOW_UI_DUMP", "/tmp/overlay_suppress.ppm", 1);
setenv("MEOW_NOTIF_ACTIVE_FILE", MARKER, 1);
setenv("MEOW_LOG_APP", SELF_ID, 1); /* 本测试进程身份=前台/后台的判定锚 */
unlink(MARKER);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
@@ -53,15 +62,15 @@ int main(void)
assert(meow_gfx_overlay_suppressed_count() == 0);
printf("T1 PASS: no marker -> presented=1 suppressed=0\n");
/* T2:有标记 → 抑制(等 >200ms 越过存在性节流缓存) */
marker_write();
/* T2:标记=其它 app_id(后台常驻 App)→ 抑制(等 >200ms 越过存在性节流缓存) */
marker_write("ncm");
usleep(250000);
unsigned long long f0 = meow_ui_frames_presented();
meow_ui_clear(0x223344);
meow_ui_flush();
assert(meow_ui_frames_presented() == f0);
assert(meow_gfx_overlay_suppressed_count() == 1);
printf("T2 PASS: marker present -> suppressed (frames frozen at %llu)\n", f0);
printf("T2 PASS: marker=other app -> suppressed (frames frozen at %llu)\n", f0);
/* T2b:抑制期再次 flush 仍抑制(节流缓存内一致) */
meow_ui_clear(0x334455);
@@ -70,24 +79,43 @@ int main(void)
assert(meow_gfx_overlay_suppressed_count() == 2);
printf("T2b PASS: still suppressed within throttle window\n");
/* T3:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏,无需重画) */
/* T2c:标记=自身 app_id(前台 App)→ 不抑制(批①前后台判定核心;恢复边沿整帧重画一次上屏) */
marker_write(SELF_ID);
usleep(250000);
meow_ui_clear(0x445566);
meow_ui_flush();
assert(meow_ui_frames_presented() == f0 + 1);
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长(前台不抑制) */
printf("T2c PASS: marker=self -> foreground, present resumed (accumulated dirty flushed)\n");
/* T3:标记=空内容(菜单/叠加层覆盖=全抑制)→ 仍抑制 */
marker_write("");
usleep(250000);
unsigned long long f1 = meow_ui_frames_presented();
meow_ui_clear(0x556677);
meow_ui_flush();
assert(meow_ui_frames_presented() == f1);
assert(meow_gfx_overlay_suppressed_count() == 3);
printf("T3 PASS: empty marker (menu/overlay) -> suppressed\n");
/* T3b:标记消失 → 恢复上屏(抑制期累积的整帧脏一次上屏) */
unlink(MARKER);
usleep(250000);
meow_ui_flush();
assert(meow_ui_frames_presented() == f0 + 1);
assert(meow_gfx_overlay_suppressed_count() == 2);
printf("T3 PASS: marker gone -> present resumed (accumulated dirty flushed)\n");
assert(meow_ui_frames_presented() == f1 + 1);
assert(meow_gfx_overlay_suppressed_count() == 3);
printf("T3b PASS: marker gone -> present resumed (accumulated dirty flushed)\n");
/* T4:系统 UI 进程(launcher)不抑制——通知中心面板本身必须照常上屏 */
meow_ui_sys_set_system_mode(1);
assert(meow_ui_sys_render_enabled() == 1);
marker_write();
marker_write("ncm");
usleep(250000);
unsigned long long f1 = meow_ui_frames_presented();
meow_ui_clear(0x445566);
unsigned long long f2 = meow_ui_frames_presented();
meow_ui_clear(0x667788);
meow_ui_flush();
assert(meow_ui_frames_presented() == f1 + 1);
assert(meow_gfx_overlay_suppressed_count() == 2); /* 不再增长 */
assert(meow_ui_frames_presented() == f2 + 1);
assert(meow_gfx_overlay_suppressed_count() == 3); /* 不再增长 */
unlink(MARKER);
printf("T4 PASS: system-UI process (launcher) never suppressed\n");
+1 -1
View File
@@ -36,7 +36,7 @@
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "../examples/hello/hello_internal.h"
#include "../hello_app/hello_internal.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>