launcher(M7.0b): 电源菜单/Now Playing 控件化 + SDK 主题薄壳 + i18n 抽取(试点迁移)

M7 整体 MD3 重写试点(M7.0 定两隔离页,只动 patch_v2/meow_launcherd/):
- 电源菜单 power_menu.c:圆角卡→meow_card;6 圆钮→meow_iconbtn(回HiBy=ARROW_BACK、
  取消=CLOSE;sb_icons 表在位图)+ meow_widget_custom_create(关机/重启/升级/锁定=pm_icons
  专有,圆形命中自定义);命中/选中/按压态全走 SDK(meow_widget_hit/set_state/draw),
  触摸按下置 PRESSED、抬起清(pm_blend 按主题混合状态层);py 手动驱动控件(哨兵 activity
  0x50 push/pop,owner 闸门使命中生效)。
- Now Playing now_playing.c:进度条→meow_progress;播放控制三钮→meow_iconbtn(SKIP_PREV /
  PLAY_ARROW|PAUSE同位双钮按状态只画其一/ SKIP_NEXT);命中走 meow_widget_hit;封面
  (meow_img contain) 与歌名/艺术家 (meow_font) 保留;哨兵 activity 0x51 push/pop。
- 主题薄壳 font_theme.c:s_ui_theme() 改读 SDK meow_theme_export()(单一权威,9 字段映射
  §8.1.5),退役 launcher 自持 4 套静态色板(#if 0 回退位)。
- i18n 抽取:两页硬编码中文案改 meow_str(新增 meow_str_zh 辅助:SDK 内嵌表无 launcher 域串
  时回退中文默认,TF 覆盖表/后续并入 SDK 内嵌后自动换语)。
- 会话骨架不动:物理键语义(单击息屏/3s 菜单/8s 强关/左缘返回/横划 NP)与两页进入退出/事件流
  未改,只替换「怎么画/怎么命中」;旧绘制函数 #if 0 留回退位。
- 验证:新增 test_power_menu_widgets / test_now_playing_widgets 宿主测试(圆形命中/owner+enabled
  闸门/选中按压态像素/主题四态 MD3+MIUIx×明暗 渲染 + meow_theme_export 九字段)全 PASS;
  launcher 全量宿主回归(lock_screen/overlay_gate/utf8_name/media/depends/ams_start/bgplay/
  settings_brightness/netmgr_ipc/vol_ramp_jack/notif_panel/notif_tf/lock_render + SDK theme_export/
  iconbtn)全 PASS;交叉 -Wall -Wextra 0 warning。新 meow_launcherd md5 94a33b7b。
This commit is contained in:
AskaEth committed 2026-09-02 17:25:21 +08:00
1 parent a8c51be1fc
commit d8241cd2d6
7 files changed
+774 -79

No files matched your search

+28
View File
@@ -741,6 +741,11 @@ void meow_font_scan_presets(int verify_sha)
}
#if 0 /* ================================================================
* M7.0b 主题退役:launcher 不再自持 4 套静态色板 —— s_ui_theme() 改读 SDK
* meow_theme_export()(单一权威:MD3/MIUIx × 明暗 由 SDK meow_theme 解析)。
* 本块保留作回退位(删除 #if 0 行即可恢复旧手绘多色板路径)。
* ================================================================ */
/* MD3 tonal palette(565 参考值见 MD3_UI_SPEC §1;此处存 24-bit RGB,#hex) */
static const meow_ui_theme_t meow_ui_theme_dark = {
0x141824, 0x1E2436, 0x263038, 0x4A6A9A, 0xFFFFFF,
@@ -817,4 +822,27 @@ const meow_ui_theme_t *s_ui_theme(void)
if (have_manual && v == 1) return light;
return dark;
}
#endif /* M7.0b 退役:launcher 4 套静态色板 → SDK meow_theme 单一权威 */
/* M7.0b 主题薄壳:launcher 主题 = SDK meow_theme_export()(MD3/MIUIx × 明暗 由 SDK 解析,
* launcher 侧不再读 ui.style/display.theme 自行选板)。只做一次拷贝 + 9 字段映射到 launcher
* 绘制便利结构 meow_ui_theme_t(§8.1.5)。返回静态缓存(调用方立即使用,不长期持有)。
* 注:meow_theme_export() 在 g_loaded==0 时先读 settings.conf,宿主/脱机也可靠。 */
const meow_ui_theme_t *s_ui_theme(void)
{
meow_theme th;
static meow_ui_theme_t s;
meow_theme_export(&th);
/* 映射:SDK meow_theme → launcher meow_ui_theme_t(9 字段,§8.1.5) */
s.surface = th.surface;
s.surface_variant = th.surfaceVariant;
s.surface_container = th.surfaceContainer;
s.primary = th.primary;
s.on_surface = th.onSurface;
s.on_surface_variant = th.onSurfaceVariant;
s.outline = th.outline;
s.error = th.error;
s.surface_tint = th.surfaceTint;
return &s;
}
@@ -43,6 +43,16 @@ extern "C" {
#include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */
#include "lock_screen.h" /* M2.x:锁屏 4 位 PIN 纯逻辑(设置/校验/错5锁定持久化/panel.pin 迁移/唤醒判定) */
/* M7.0b 文案抽取(SDK meow_str i18n 雏形):`meow_str(id)` 在 id 未进内嵌表(且无 TF 覆盖表)
* 时原样返回含'.'的 id —— 系统 UI 硬编码中文案经本辅助抽取,未知 id 回退 `zh` 兜底,保证默认
* 中文不被裸 id 顶替;其 en/自定义文案可由 TF `strings/<locale>.txt` 覆盖表或后续并入 SDK 内嵌表补齐。
* 注:static inline 不跨 TU 产生未用告警(GCC 忽略 `unused static inline`)。 */
static inline const char *meow_str_zh(const char *id, const char *zh)
{
const char *s = meow_str(id);
return (s && s[0] && !strchr(s, '.')) ? s : zh;
}
#define DEV_POWER "/dev/input/event0" /* md-gpio-keys: KEY_POWER, KEY_PREVIOUS */
#define DEV_KEYS "/dev/input/event2" /* jz adc keyboard: VOL, PLAY, NEXT */
#define DEV_TOUCH "/dev/input/event1" /* goodix-ts (not grabbed, shareable) */
Binary file not shown.
+107 -48
View File
@@ -47,6 +47,10 @@ static void np_cover_load(const meow_media_session_t *s)
g_np_cover = meow_img_load_file(path);
}
#if 0 /* ================================================================
* M7.0b 退役:Now Playing 手绘控制钮/进度条改走 SDK 控件(meow_iconbtn +
* meow_progress),命中/状态/进度全由 SDK 接管;保留作回退位(删除 #if 0 行恢复旧手绘)。
* ================================================================ */
/* 实心三角(dir=+1 右向 / -1 左向)。 */
static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
{
@@ -59,25 +63,6 @@ static void np_tri(int cx, int cy, int size, int dir, uint32_t col)
}
}
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。 */
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
int W = fb_xres;
if (ui_width(s, size) <= maxw) {
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
return;
}
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
}
/* 圆形控制按钮(kind:0=prev 1=play 2=next 3=pause)。 */
static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
@@ -97,14 +82,94 @@ static void np_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
fill_rect(cx + 8 - bw, cy - (r - 7), bw, (r - 7) * 2, fg);
}
}
#endif /* M7.0b 退役:Now Playing 旧手绘控制钮 */
/* 进度带(进度条 + 时间)绘制;pts = 当前显示位置。 */
/* 居中 + 按宽截断(UTF-8 安全),防超长标题溢出屏外。(M7.0b 保留:封面/歌名/艺术家仍 SD) */
static void np_center_text(int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
int W = fb_xres;
if (ui_width(s, size) <= maxw) {
ui_draw((W - ui_width(s, size)) / 2, y, s, color, size);
return;
}
/* 近似截断:实际按字节长度比例切(title/artist 罕见超长,够用不裂屏) */
char buf[160];
int keep = (int)(strlen(s) * maxw / (ui_width(s, size) + 1));
if (keep < 0) keep = 0;
if (keep > (int)sizeof buf - 4) keep = (int)sizeof buf - 4;
memcpy(buf, s, (size_t)keep); buf[keep] = 0;
strcat(buf, "...");
ui_draw((W - ui_width(buf, size)) / 2, y, buf, color, size);
}
/* ==================================================================
* M7.0b Now Playing 控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 进度条 → meow_progress(线性,SDK 主题 track/fill);
* - 播放控制三钮 → meow_iconbtn(PREV / PLAY_ARROW|PAUSE / NEXT;按下=PAUSE、暂停=PLAY_ARROW,
* 同位置叠两钮、按当前状态只画其一);
* - 命中走 meow_widget_hit(圆形,owner==哨兵 + enabled 闸门)→ meow_media_playctl 分发;
* - 封面(meow_img contain)与歌名/艺术家(meow_font)不动(本就 SD),仅文案走 meow_str。
* 控件 lazy 创建一次(owner=NP_SENTINEL_ACT),跨多次进入复用;run_now_playing() 进入 push
* 哨兵、退出 pop(owner 闸门使控件命中生效)。
* ================================================================== */
#define NP_SENTINEL_ACT 0x51 /* Now Playing 哨兵 activity */
static meow_widget *g_np_prog = NULL;
static meow_widget *g_np_prev = NULL, *g_np_play = NULL, *g_np_pause = NULL, *g_np_next = NULL;
static int g_np_init = 0;
static void np_widgets_create(void)
{
meow_ui_activity_push(NP_SENTINEL_ACT);
g_np_prog = meow_progress_create(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, 0);
g_np_prev = meow_iconbtn_create(NP_PREV_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_PREV);
g_np_play = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PLAY_ARROW);
g_np_pause = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PAUSE);
g_np_next = meow_iconbtn_create(NP_NEXT_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮保持「强调动作」观感:底色=primary、图标=onPrimary;其余两钮默认 surfaceContainerLow。 */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
meow_ui_activity_pop();
g_np_init = 1;
}
/* 主题刷新:播放钮底色/图标随当前主题(明暗/风格切换后跟手)。 */
static void np_refresh_theme(void)
{
if (!g_np_init) return;
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
if (g_np_play) meow_widget_set_opts(g_np_play, &op);
if (g_np_pause) meow_widget_set_opts(g_np_pause, &op);
}
/* 绘制播放控制三钮(prev/next 固定;play/pause 按会话状态只画其一——两钮同位,改画时
* 上一帧残留被新画圆底盖掉)。 */
static void np_draw_ctl(const meow_media_session_t *s)
{
np_refresh_theme();
if (g_np_prev) { meow_widget_set_selected(g_np_prev, 0); meow_widget_draw(g_np_prev); }
if (g_np_next) { meow_widget_set_selected(g_np_next, 0); meow_widget_draw(g_np_next); }
int playing = (s && s->state == MEOW_MEDIA_PLAYING);
meow_widget *pb = playing ? g_np_pause : g_np_play;
if (pb) { meow_widget_set_selected(pb, 0); meow_widget_draw(pb); }
}
/* 进度带(进度条 + 时间)绘制;pos = 当前显示位置。进度条走 SDK meow_progress。 */
static void np_draw_progress(const meow_media_session_t *s, uint32_t pos, const meow_ui_theme_t *th)
{
fill_rect(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, th->outline);
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * NP_PROG_W / s->dur_ms) : 0;
if (fw > NP_PROG_W) fw = NP_PROG_W;
if (fw > 0) fill_rect(NP_PROG_X, NP_PROG_Y, fw, 6, th->primary);
int pct = (s->dur_ms > 0) ? (int)((uint64_t)pos * 100 / s->dur_ms) : 0;
if (pct > 100) pct = 100;
if (g_np_prog) { meow_progress_set(g_np_prog, pct); meow_widget_draw(g_np_prog); }
/* 时间(左=当前,右=总长;MM:SS) */
char l[16], rbuf[16];
unsigned lm = pos / 60000u, ls = (pos % 60000u) / 1000u;
@@ -120,16 +185,17 @@ static void np_draw_page(int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
if (!g_np_init) np_widgets_create(); /* M7.0b:lazy 建控件(跨进入复用) */
if (redraw_sb) fill_rect(0, 0, W, SB_BAR_H, th->surface);
fill_rect(0, SB_BAR_H, W, H - SB_BAR_H, th->surface);
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) {
/* 空态占位 */
const char *t1 = "未在播放";
/* 空态占位(M7.0b 文案走 meow_str,未知 id 回退中文) */
const char *t1 = meow_str_zh("np.not_playing", "未在播放");
ui_draw((W - ui_width(t1, 26)) / 2, 300, t1, th->on_surface_variant, 26);
const char *t2 = "打开播放 App 以显示正在播放";
const char *t2 = meow_str_zh("np.open_app_hint", "打开播放 App 以显示正在播放");
ui_draw((W - ui_width(t2, 18)) / 2, 348, t2, th->on_surface_variant, 18);
const char *t3 = "左缘右滑返回";
const char *t3 = meow_str_zh("np.edge_back", "左缘右滑返回");
ui_draw((W - ui_width(t3, 16)) / 2, 396, t3, th->on_surface_variant, 16);
if (redraw_sb) draw_statusbar();
return;
@@ -152,16 +218,13 @@ static void np_draw_page(int redraw_sb)
np_center_text(NP_ARTIST_Y, s->artist, 18, th->on_surface_variant, W - 60);
if (s->album[0])
np_center_text(NP_ALBUM_Y, s->album, 16, th->on_surface_variant, W - 80);
/* 进度 */
/* 进度(SDK meow_progress) */
uint32_t pos = meow_media_display_pos(s);
np_draw_progress(s, pos, th);
/* 控制按钮 */
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1;
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, pk, th);
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
/* 歌词占位 */
const char *ly = "歌词(占位)";
/* 控制按钮(SDK meow_iconbtn,命中/状态走 SDK) */
np_draw_ctl(s);
/* 歌词占位(M7.0b 文案走 meow_str) */
const char *ly = meow_str_zh("np.lyric_placeholder", "歌词(占位)");
ui_draw((W - ui_width(ly, 16)) / 2, NP_LYRIC_Y, ly, th->on_surface_variant, 16);
if (redraw_sb) draw_statusbar();
}
@@ -176,25 +239,20 @@ static void np_repaint_progress(void)
/* 清进度带 + 控制带,再重绘(只脏这两带,不整屏)。 */
fill_rect(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y, th->surface);
np_draw_progress(s, pos, th);
np_btn(NP_PREV_CX, NP_BTN_Y, NP_PREV_R, 0, th);
np_btn(NP_PLAY_CX, NP_BTN_Y, NP_PLAY_R, (s->state == MEOW_MEDIA_PLAYING) ? 3 : 1, th);
np_btn(NP_NEXT_CX, NP_BTN_Y, NP_PREV_R, 2, th);
np_draw_ctl(s);
meow_dirty_add(0, NP_TIME_Y, fb_xres, NP_BTN_Y + NP_PLAY_R - NP_TIME_Y);
meow_ui_flush();
}
/* 控制按钮命中:返回 cmd(1-4),0=未命中。 */
/* 控制按钮命中:返回 cmd(1-4),0=未命中。走 SDK 圆形命中(owner==哨兵 + enabled 闸门)。
* 播放/暂停两钮同位:命中任一(同矩形)→ 按会话状态分派 play(1)/pause(2)。 */
static int np_btn_tap(int tx, int ty)
{
if (tx >= NP_PREV_CX - NP_PREV_R && tx <= NP_PREV_CX + NP_PREV_R &&
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 3;
if (tx >= NP_PLAY_CX - NP_PLAY_R && tx <= NP_PLAY_CX + NP_PLAY_R &&
ty >= NP_BTN_Y - NP_PLAY_R && ty <= NP_BTN_Y + NP_PLAY_R) {
const meow_media_session_t *s = meow_media_current();
const meow_media_session_t *s = meow_media_current();
if (g_np_prev && meow_widget_hit(g_np_prev, tx, ty)) return 3;
if (g_np_play && meow_widget_hit(g_np_play, tx, ty))
return (s && s->state == MEOW_MEDIA_PLAYING) ? 2 : 1;
}
if (tx >= NP_NEXT_CX - NP_PREV_R && tx <= NP_NEXT_CX + NP_PREV_R &&
ty >= NP_BTN_Y - NP_PREV_R && ty <= NP_BTN_Y + NP_PREV_R) return 4;
if (g_np_next && meow_widget_hit(g_np_next, tx, ty)) return 4;
return 0;
}
@@ -351,6 +409,7 @@ void run_now_playing(void)
}
usleep(10000);
}
meow_ui_activity_pop(); /* M7.0b:退出 Now Playing,回主 activity(控件 owner 仍钉哨兵,复用) */
if (tfd >= 0) close(tfd);
kp_close(&kp_keys);
kp_close(&kp_power);
+219 -31
View File
@@ -41,6 +41,10 @@ static void blit_pm_icon(int idx, int cx, int cy, uint16_t col)
}
#if 0 /* ================================================================
* M7.0b 退役:电源菜单圆钮改走 SDK 控件(meow_card/meow_iconbtn/自定义画布),
* 命中/选中/按压态全由 SDK 接管;本块保留作回退位(删除 #if 0 行恢复旧手绘)。
* ================================================================ */
/* 绘制电源菜单圆钮 i(MD3 圆形:外描边+内填充+圆内 Material 图标;选中高亮)。
* 圆内放 Material Symbols 位图,文字标签放圆下(Android 12 式:icon 上、label 下)。 */
static void draw_pm_button(int i, int selected)
@@ -71,6 +75,180 @@ static void draw_pm_button(int i, int selected)
draw_text(cx - lw / 2, ly + 1, lab, 1, col);
}
}
#endif /* M7.0b 退役:电源菜单旧手绘圆钮 */
/* ==================================================================
* M7.0b 电源菜单控件化(launcher 无 meow_ui_run,手动驱动 SDK 控件):
* - 圆角卡 → meow_card(SDK 主题 surfaceContainerLow);
* - 六个圆钮 → meow_iconbtn(图标在 SDK sb_icons 表:回HiBy→ARROW_BACK、取消→CLOSE)
* 或 meow_widget_custom_create(pm_icons 专有位图:关机/重启/升级/锁定);
* - 命中/选中/按压态全走 SDK(meow_widget_hit/set_state/draw);
* - 主题 = 读 SDK meow_theme(单一权威),后台遮罩仍 dim_screen(会话骨架不动)。
* 控件 lazy 创建一次(owner=PM_SENTINEL_ACT 哨兵 activity),跨多次进入复用;
* show_power_menu() 进入时 push 哨兵、退出时 pop(owner 闸门使控件命中生效)。
* ================================================================== */
#define PM_SENTINEL_ACT 0x50 /* 电源菜单哨兵 activity(不与 launcher 其它页冲突) */
#define PM_BTN_SIZE (PM_R * 2) /* 圆钮 = 外接正方形边长(直径) */
/* 哪些 pm 图标在 SDK sb_icons 表(可用 meow_iconbtn 直接出图);-1=不在 → 自定义画布 blit pm_icons */
static const int pm_sdk_icon[PM_NUM] = {
-1, /* 0 关机:pm_icons 专有(SDK 无 power 位图) */
-1, /* 1 重启:pm_icons 专有 */
MEOW_ICON_ARROW_BACK, /* 2 回HiBy:SB_ARROW_BACK 在表 */
-1, /* 3 升级:pm_icons 专有 */
-1, /* 4 锁定:pm_icons 专有(SDK 无 lock 位图) */
MEOW_ICON_CLOSE, /* 5 取消:SB_CLOSE 在表 */
};
/* 圆钮文案 i18n id(meow_str)。common.cancel 内嵌已有;power.* 需 TF 覆盖表/后续并入 SDK 内嵌。 */
static const char *pm_str_id[PM_NUM] = {
"power.off", "power.restart", "power.back_hiby",
"power.upgrade", "power.lock", "common.cancel",
};
static meow_widget *g_pm_card = NULL;
static meow_widget *g_pm_btns[PM_NUM] = {NULL};
static int g_pm_init = 0;
/* 按主题混合(pct 0..100:base 向 top 混 pct%,24-bit 每通道;近似 SDK state_tint/blend)。 */
static uint32_t pm_blend(uint32_t base, uint32_t top, int pct)
{
int br = (base >> 16) & 0xFF, bg = (base >> 8) & 0xFF, bb = base & 0xFF;
int tr = (top >> 16) & 0xFF, tg = (top >> 8) & 0xFF, tb = top & 0xFF;
int r = (br * (100 - pct) + tr * pct) / 100;
int g = (bg * (100 - pct) + tg * pct) / 100;
int b = (bb * (100 - pct) + tb * pct) / 100;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 自定义画布:绘制 pm_icons 专有圆钮(图标不在 SDK sb_icons 表时用)。
* SDK 已把绘制 clip 设为控件矩形;圆形底色按 selected 取主题容器色,图标墨色随之;
* 禁用/按压态由本回调自叠(SDK 不替 W_CUSTOM 自动叠,meow_widget_draw_state_layer 铺方矩形,
* 圆形钮此处手工按圆域混合,更贴合)。 */
static void pm_render_cb(meow_widget *w, const meow_theme *th, void *ctx)
{
int i = (int)(intptr_t)ctx;
int rc[4]; meow_widget_rect(w, rc); /* 绝对坐标 x,y,w,h(launcher 侧 meow_widget 不透明) */
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
unsigned st = meow_widget_state(w);
int sel = (st & MEOW_WS_SELECTED) != 0;
int disabled = (st & MEOW_WS_ENABLED) == 0;
int pressed = (st & MEOW_WS_PRESSED) != 0;
uint32_t base = sel ? th->primaryContainer : th->surfaceContainer;
uint32_t fg = sel ? th->onPrimaryContainer : th->onSurface;
if (disabled) base = pm_blend(base, th->onSurface, 38); /* 禁用变暗(向灰) */
else if (pressed) base = pm_blend(base, th->onSurface, sel ? 24 : 12); /* 按压状态层 */
meow_gfx_fill_circle(cx, cy, r, base);
blit_pm_icon(i, cx, cy, rgb565(fg));
}
/* 自定义画布命中:圆形(圆心=控件中心,半径=size/2)。 */
static int pm_hit_cb(meow_widget *w, int x, int y, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
int dx = x - cx, dy = y - cy;
return dx * dx + dy * dy <= r * r;
}
/* 创建卡片 + 六钮(owner 钉 = PM_SENTINEL_ACT,仅首进创建一次,复用)。 */
static void pm_widgets_create(void)
{
meow_ui_activity_push(PM_SENTINEL_ACT);
g_pm_card = meow_card_create(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H);
for (int i = 0; i < PM_NUM; i++) {
int sz = PM_BTN_SIZE;
int x = pm_cx[i] - sz / 2, y = pm_cy[i] - sz / 2;
if (pm_sdk_icon[i] >= 0)
g_pm_btns[i] = meow_iconbtn_create(x, y, sz, pm_sdk_icon[i]);
else
g_pm_btns[i] = meow_widget_custom_create(x, y, sz, sz,
pm_render_cb, pm_hit_cb,
(void *)(intptr_t)i);
}
meow_ui_activity_pop(); /* 回主 activity(控件 owner 已钉为哨兵) */
g_pm_init = 1;
}
/* 主题刷新:iconbtn 底色=surfaceContainer(与卡片 surfaceContainerLow 拉开对比)。
* custom 钮由 render_cb 读 th 自带;此处只补 iconbtn 的 opts.bg(跨开放切主题也跟手)。 */
static void pm_refresh_theme(void)
{
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->surfaceContainer;
for (int i = 0; i < PM_NUM; i++)
if (g_pm_btns[i] && pm_sdk_icon[i] >= 0)
meow_widget_set_opts(g_pm_btns[i], &op);
}
/* i18n 文案(meow_str_zh:未知 id → 中文兜底)。TF 覆盖表/后续 SDK 内嵌并入后自动换语。 */
static const char *pm_label(int i)
{
return meow_str_zh(pm_str_id[i], pm_labels_cn[i]);
}
/* 绘制单个圆钮(SDK 控件)+ 圆下标签(主题色,选中亮/未选次色)。 */
static void pm_draw_button(int i, int selected)
{
meow_widget *w = g_pm_btns[i];
if (!w) return;
meow_widget_set_selected(w, selected);
meow_widget_draw(w);
const meow_ui_theme_t *th = s_ui_theme();
int cx = pm_cx[i], cy = pm_cy[i];
int ly = cy + PM_R + 8;
uint32_t col = selected ? th->on_surface : th->on_surface_variant;
if (mfont_ready) {
const char *lab = pm_label(i);
int lw = meow_font_width_text(lab, 18);
meow_font_drawtext((uint16_t *)fb, cx - lw / 2, ly, lab, rgb565(col), 18);
} else {
const char *lab = pm_labels_en[i];
draw_text(cx - (int)strlen(lab) * 5 / 2, ly + 1, lab, 1, col);
}
}
/* 标脏一个圆钮的整块区域(圆外接框 + 圆下标签),供局部 flush。 */
static void pm_button_dirty(int i)
{
int sz = PM_BTN_SIZE, cy = pm_cy[i];
meow_dirty_add(pm_cx[i] - sz / 2 - 4, cy - sz / 2 - 4, sz + 8, sz + PM_R + 44);
}
/* 命中检测:返回命中的钮下标(0..PM_NUM-1),否则 -1。 */
static int pm_hit_button(int x, int y)
{
for (int i = 0; i < PM_NUM; i++)
if (g_pm_btns[i] && meow_widget_hit(g_pm_btns[i], x, y)) return i;
return -1;
}
/* 绘制卡片 + 标题 + 底部提示 + 全部圆钮(进入/全刷时调用一次)。 */
static void pm_draw_card(void)
{
pm_refresh_theme();
if (g_pm_card) meow_widget_draw(g_pm_card);
const meow_ui_theme_t *th = s_ui_theme();
const char *title = "POWER MENU";
int title_y = PM_CARD_Y0 + 26;
if (mfont_ready)
meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2,
title_y, title, rgb565(th->on_surface), 20);
else
draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, th->on_surface);
const char *hint = meow_str_zh("power.hint_nav", "音量导航 电源确认");
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
if (mfont_ready)
meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(hint, 18)) / 2,
hint_y, hint, rgb565(th->on_surface_variant), 18);
else
draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2,
hint_y + 1, "VOLUME NAV POWER OK", 1, th->on_surface_variant);
}
/* ---- 电源管理服务 IPC 客户端(SYSTEM_FRAMEWORK_SERVICES §3.2:launcher 经服务触发电源动作)----
* launcher 不再直连 reboot(2)syscall/写 mtd5/写 boot 默认 —— 统一经 meow_svc_power 的
@@ -191,19 +369,12 @@ int show_power_menu(void)
/* M2.11 B:默认光标 = 「取消」(安全项,index 4)。用户打开菜单后未导航就按电源/播放,
* 只会取消关菜单,绝不因「按电源键」本身关机/重启。要真关机必须先导航到「关机」再确认。 */
int sel = PM_NUM - 1; /* 默认取消(安全) */
/* M2.30 MD3:居中圆角卡片背景 + 标题居中 + 圆钮网格。卡片白色柔和描边/深色底。 */
fill_round_rect(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H, PM_CARD_R, 0x141824);
fill_round_rect(PM_CARD_X0 + 2, PM_CARD_Y0 + 2, PM_CARD_W - 4, PM_CARD_H - 4, PM_CARD_R - 2, 0x1C2438);
/* 卡片顶部标题,水平居中 */
char title[32];
snprintf(title, sizeof title, "POWER MENU");
int title_y = PM_CARD_Y0 + 26; /* 标题在卡片内顶部(卡片顶 115,标题基线 ~141) */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text(title, 20)) / 2, title_y, title, rgb565(0xFFFFFF), 20);
else draw_text((fb_xres - (int)strlen(title) * 6) / 2, title_y, title, 2, 0xFFFFFF);
/* 底部提示行(卡片下方,非圆钮所需) */
int hint_y = PM_CARD_Y0 + PM_CARD_H + 10;
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, (fb_xres - meow_font_width_text("音量导航 电源确认", 18)) / 2, hint_y, "音量导航 电源确认", rgb565(0x777799), 18);
else draw_text((fb_xres - (int)strlen("VOLUME NAV POWER OK") * 5) / 2, hint_y + 1, "VOLUME NAV POWER OK", 1, 0x777799);
/* M7.0b 控件化:进入 push 哨兵(控件 owner 闸门使 meow_widget_hit 生效)+ lazy 建卡/钮。
* 卡片=meow_card(SDK 主题 surfaceContainerLow),圆钮=meow_iconbtn/自定义画布,命中/选中/
* 按压态全走 SDK;背景遮罩仍 dim_screen(会话骨架不动)。 */
meow_ui_activity_push(PM_SENTINEL_ACT);
if (!g_pm_init) pm_widgets_create();
pm_draw_card(); /* 卡片 + 标题 POWER MENU + 底部提示(SDK 主题色,i18n 文案) */
meow_dirty_full(); /* M2.16:把压暗列表+卡片+标题+(下方圆钮)作为一整帧呈现 */
/* 触摸状态(按下→抬起):统一 meow_touch 接口(goodix 时序免疫,原点由首个有效 ABS 锁) */
@@ -230,19 +401,18 @@ int show_power_menu(void)
* 取消 8s 强关,让所选动作(如「回到HibyOS」→ reboot RESTART)生效。仅当全程未互动才保留「按住
* 8s=强关」。*/
int pm_interacted = 0;
int pm_press_idx = -1; /* M7.0b:当前按下钮(按压态走 SDK 状态机位) */
for (;;) {
/* dirty 绘制:首次全画;此后仅选中变化才重画受影响两钮(消频闪)→ 标脏 + present。 */
if (!pm_init) {
for (int i = 0; i < PM_NUM; i++) draw_pm_button(i, i == sel);
for (int i = 0; i < PM_NUM; i++) pm_draw_button(i, i == sel);
pm_init = 1; last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present(vsync+帧率门控+blit 脏=全帧,含压暗列表+标题+按钮) */
} else if (sel != last_sel) {
draw_pm_button(last_sel, 0);
draw_pm_button(sel, 1);
/* M2.30 圆形:dirty 区 = 圆(圆心±半径)+ 圆下文字(半径+偏移+行高),四周留 10px */
int dr = (PM_R + 6) + 10, dtxt = PM_R + 26 + 10;
meow_dirty_add(pm_cx[last_sel] - dr, pm_cy[last_sel] - dr, dr * 2, dr + dtxt);
meow_dirty_add(pm_cx[sel] - dr, pm_cy[sel] - dr, dr * 2, dr + dtxt);
pm_draw_button(last_sel, 0);
pm_draw_button(sel, 1);
pm_button_dirty(last_sel);
pm_button_dirty(sel);
last_sel = sel;
meow_ui_flush(); /* A-1/A-2:SDK present,blit 两钮区 */
}
@@ -280,19 +450,31 @@ int show_power_menu(void)
mt_down(&mt);
pm_interacted = 1;
pm_last_interact = now_ms();
/* M7.0b 按压态:按下命中钮 → 置 PRESSED 并重绘(SDK 状态层,实时) */
pm_press_idx = pm_hit_button(mt.down_x, mt.down_y);
if (pm_press_idx >= 0) {
meow_widget_set_state(g_pm_btns[pm_press_idx], MEOW_WS_PRESSED, 1);
meow_widget_draw(g_pm_btns[pm_press_idx]);
pm_button_dirty(pm_press_idx);
meow_ui_flush();
}
} else if (!val && mt.down) {
pm_interacted = 1;
pm_last_interact = now_ms();
if (pm_press_idx >= 0) { /* 清按压态(SDK 状态层随 PRESSED 位清除) */
meow_widget_set_state(g_pm_btns[pm_press_idx], MEOW_WS_PRESSED, 0);
meow_widget_draw(g_pm_btns[pm_press_idx]);
pm_button_dirty(pm_press_idx);
pm_press_idx = -1;
meow_ui_flush();
}
int dx, dy;
/* M2.25:抬手位移 >= MEOW_TOUCH_SWIPE_THRESH 判滑动(返回0),
* 不做确认;否则有效 tap,按下原点命中的按钮即确认。 */
if (mt_up(&mt, &dx, &dy)) {
/* M2.30 圆形命中:按下原点距圆心 <= PM_R+6 即命中 */
for (int i = 0; i < PM_NUM; i++) {
int ddx = mt.down_x - pm_cx[i], ddy = mt.down_y - pm_cy[i];
int rr = (PM_R + 6) * (PM_R + 6);
if (ddx * ddx + ddy * ddy <= rr) { sel = i; act = i; break; }
}
/* M7.0b 命中走 SDK(owner==哨兵 + enabled + 圆形命中) */
int hit = pm_hit_button(mt.down_x, mt.down_y);
if (hit >= 0) { sel = hit; act = hit; }
}
}
}
@@ -303,12 +485,13 @@ int show_power_menu(void)
/* 8s 强关(绕过):仅当电源持续按住、且用户全程未与菜单互动才触发 —— 放在触摸/按键处理
* 之后,避免「用户已点选按钮」的同一轮被强关抢先(M2.10 Bug)。 */
if (pwr && !pm_interacted && now_ms() - pm_t0 >= 5000) {
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); return 1; }
if (sim_no_exec) { logmsg("pm: [sim-no-exec] POWER HOLD 8s -> would force poweroff"); meow_ui_activity_pop(); return 1; }
logmsg("pm: POWER HOLD 8s -> force poweroff");
if (meow_power_action(POWER_SHUTDOWN) != MEOW_OK) {
logmsg("pm: power service unavailable, abort force-off (menu closed, no action)");
return 0;
meow_ui_activity_pop(); return 0;
}
meow_ui_activity_pop();
return 1;
}
@@ -328,6 +511,7 @@ int show_power_menu(void)
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
meow_ui_activity_pop(); /* M7.0b:退出电源菜单,回主 activity(控件 owner 仍钉哨兵,复用) */
/* M2.11 B:电源键在菜单 = 按当前项确认。act 与电源动作逐项核对:
* act==PM_NUM-1(取消) -> 只关菜单,绝不关机(return 0,永不到电源动作);
@@ -391,7 +575,9 @@ static void draw_cr_button(int sel, int i)
{
int x0 = (i == 0) ? 80 : 280, y0 = 420, w = 120, h = 60;
fill_round_rect(x0, y0, w, h, 12, (i == sel) ? 0x2A3A6A : 0x1A1A2A);
const char *lbl = (i == 0) ? "是" : "否";
/* M7.0b i18n:是/否 经 meow_str 抽取(未知 id 回退中文) */
const char *lbl = (i == 0) ? meow_str_zh("power.confirm_yes", "是")
: meow_str_zh("power.confirm_no", "否");
if (mfont_ready) {
int lw = meow_font_width_text(lbl, 20);
meow_font_drawtext((uint16_t *)fb, x0 + w / 2 - lw / 2, y0 + 30 - meow_font_line_height(20) / 2,
@@ -408,7 +594,8 @@ int confirm_reboot_player(void)
/* M2.8 Bug 3-residual:确认框同电源菜单 —— 若打开前刚经历息屏/blank,LCDC 扫描地址可能
* 非 buffer0;强制 re-pan 让「压暗列表 + 是/否」落在被显示的缓冲。 */
fb_pan_buf0_force();
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320, "返回播放器需重启,现在重启?", rgb565(0xFFFFFF), 20);
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 320,
meow_str_zh("power.reboot_confirm", "返回播放器需重启,现在重启?"), rgb565(0xFFFFFF), 20);
else draw_text(20, 322, "REBOOT TO RETURN TO PLAYER?", 2, 0xFFFFFF);
meow_touch mt;
@@ -428,7 +615,8 @@ int confirm_reboot_player(void)
int cr_init = 0, cr_last_sel = -1; /* M2.9 Bug 4:dirty——仅 sel 变化才重画两钮 */
/* 标题/按键提示:打开时画一次(原实现把它们放主循环每 10ms 全刷,与 LCD 刷新竞争 →
* 「是/否」钮频闪;改为只在打开/状态变化时绘制)。标题在前面已画一次,这里画提示行。 */
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560, "音量切换 电源取消", rgb565(0x666688), 20);
if (mfont_ready) meow_font_drawtext((uint16_t *)fb, 20, 560,
meow_str_zh("power.confirm_hint", "音量切换 电源取消"), rgb565(0x666688), 20);
else draw_text(20, 562, "VOL NAV POWER EXIT", 1, 0x666688);
meow_dirty_full(); /* M2.16:压暗列表+标题+提示作为一整帧呈现 */
for (;;) {
@@ -0,0 +1,175 @@
/* test_now_playing_widgets.c — M7.0b Now Playing 控件化宿主验证。
*
* 复现 run_now_playing() 所建控件集(np_widgets_create:meow_progress 进度条 + meow_iconbtn
* 三钮,play/pause 同位双钮按状态只画其一),验证:
* N1 进度条:value clamp 0-100 + track/fill 渲染(填充分界像素不同);
* N2 三钮圆形命中:圆心命中、角部不命中;
* N3 play/pause 同位切换:两钮图标位图不同 → 中心像素不同(画 PAUSE vs PLAY_ARROW);
* N4 主题四态(MD3/MIUIx × 明暗)下进度条+三钮渲染非背景像素。
*
* 几何常量与 now_playing.c 同源(镜像);控件创建方式与 np_widgets_create 同。
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -o test_now_playing_widgets test_now_playing_widgets.c \
* ../sdk/test_host_stub.c ../sdk/meow_ui_gfx.c ../sdk/meow_ui_sys.c \
* ../sdk/meow_ui_theme.c ../sdk/meow_ui_vol.c ../sdk/meow_ui_log.c \
* ../sdk/meow_ui_widget.c ../sdk/meow_ui_input.c ../sdk/meow_ui_activity.c \
* ../sdk/meow_ui_dialog.c ../sdk/meow_ui_ime.c ../sdk/meow_ui_str.c ../sdk/meow_pinyin.c \
* ../sdk/meow_ui_draw.c ../sdk/meow_ui_view.c ../sdk/meow_ui_box.c ../sdk/meow_ui_anim.c \
* ../sdk/meow_ui_scroll.c ../sdk/meow_ui_img.c ../sdk/meow_ui_media.c \
* ../sdk/meow_notify_ipc.c ../sdk/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_now_playing_widgets
*/
#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 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)
/* ---- 与 now_playing.c 同源几何(镜像) ---- */
#define NP_PROG_X 40
#define NP_PROG_W (480 - 80) /* 400 */
#define NP_PROG_Y 518
#define NP_TIME_Y 492
#define NP_BTN_Y 568
#define NP_PLAY_CX 240
#define NP_PREV_CX (NP_PLAY_CX - 78) /* 162 */
#define NP_NEXT_CX (NP_PLAY_CX + 78) /* 318 */
#define NP_PLAY_R 30
#define NP_PREV_R 22
#define NP_SENTINEL 0x51
static meow_widget *prog = NULL, *prev = NULL, *play = NULL, *pause = NULL, *next = NULL;
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(int x, int y) { unsigned short p; rd_px(x, y, &p); return p; }
static int region_nonbg(int x, int y, int w, int h, uint32_t bg)
{
unsigned short bg565 = ((bg >> 8) & 0xF800) | ((bg >> 5) & 0x07E0) | ((bg >> 3) & 0x001F);
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px565(x + dx, y + dy) != bg565) cnt++;
return cnt;
}
static void mk_widgets(void)
{
meow_ui_activity_push(NP_SENTINEL);
prog = meow_progress_create(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, 0);
prev = meow_iconbtn_create(NP_PREV_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_PREV);
play = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PLAY_ARROW);
pause = meow_iconbtn_create(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, MEOW_ICON_PAUSE);
next = meow_iconbtn_create(NP_NEXT_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R,
NP_PREV_R * 2, MEOW_ICON_SKIP_NEXT);
/* 主播放钮强调:primary/onPrimary(镜像 np_widgets_create) */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->primary; op.fg = th->onPrimary;
meow_widget_set_opts(play, &op);
meow_widget_set_opts(pause, &op);
meow_ui_activity_pop();
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/nowplaying.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
meow_ui_activity_push(NP_SENTINEL);
mk_widgets();
const meow_theme *th = meow_theme_get();
meow_ui_set_style(MEOW_STYLE_MD3);
meow_theme_set_mode(MEOW_THEME_DARK);
th = meow_theme_get();
/* ============ N1 进度条 clamp + track/fill 渲染 ============ */
printf("N1 progress value + track/fill\n");
meow_widget_set_opts(prog, &(meow_widget_opts){ .bg = 0 });
meow_ui_clear(0);
meow_progress_set(prog, 150); /* clamp → 100 */
meow_widget_draw(prog);
unsigned short fill_end = px565(NP_PROG_X + NP_PROG_W - 2, NP_PROG_Y + 3);
unsigned short track_mid = px565(NP_PROG_X + NP_PROG_W / 2, NP_PROG_Y + 3);
CHECK(fill_end != 0 && track_mid != 0, "value 150 clamp 100 -> full fill renders");
meow_progress_set(prog, 50);
meow_widget_draw(prog);
/* 50% 填充区 [40,240) 内像素应为主色;240 之后的轨道区应为轨道色(surfaceVariant) */
unsigned short infill = px565(NP_PROG_X + NP_PROG_W / 4, NP_PROG_Y + 3); /* x=140,填充区 */
unsigned short empty = px565(NP_PROG_X + NP_PROG_W - 4, NP_PROG_Y + 3); /* x=436,轨道区 */
CHECK(infill != empty, "value 50 -> fill region differs from empty tail");
/* ============ N2 三钮圆形命中 ============ */
printf("N2 circular hit\n");
CHECK(meow_widget_hit(prev, NP_PREV_CX, NP_BTN_Y), "prev center hits");
CHECK(!meow_widget_hit(prev, NP_PREV_CX - NP_PREV_R, NP_BTN_Y - NP_PREV_R), "prev corner MISSES");
CHECK(meow_widget_hit(play, NP_PLAY_CX, NP_BTN_Y), "play center hits");
CHECK(meow_widget_hit(pause, NP_PLAY_CX, NP_BTN_Y), "pause(overlay) center hits");
CHECK(!meow_widget_hit(play, NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R), "play corner MISSES");
CHECK(meow_widget_hit(next, NP_NEXT_CX, NP_BTN_Y), "next center hits");
/* ============ N3 play/pause 同位切换(两钮位图不同 → 像素不同) ============ */
printf("N3 play/pause overlay icon switch\n");
meow_ui_clear(0);
meow_widget_draw(play); meow_ui_clear(0); /* 只画 play */
meow_widget_draw(play);
unsigned short p_play = px565(NP_PLAY_CX, NP_BTN_Y); /* 中心=图标墨色 */
meow_ui_clear(0);
meow_widget_draw(pause);
unsigned short p_pause = px565(NP_PLAY_CX, NP_BTN_Y);
CHECK(p_play != p_pause, "play vs pause icon differs at centre (overlay switch works)");
/* 圆心区域非页面底色(画出来了) */
meow_ui_clear(0); meow_widget_draw(pause);
CHECK(region_nonbg(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, NP_PLAY_R * 2, th->surface) > 0,
"pause button renders non-bg in circle");
/* ============ N4 主题四态:进度条+三钮渲染非背景 ============ */
printf("N4 theme 4-state render\n");
const int styles[2] = { MEOW_STYLE_MD3, MEOW_STYLE_MIUIX };
const int modes[2] = { MEOW_THEME_LIGHT, MEOW_THEME_DARK };
for (int si = 0; si < 2; si++) {
for (int mi = 0; mi < 2; mi++) {
meow_ui_set_style(styles[si]);
meow_theme_set_mode((meow_theme_mode)modes[mi]);
th = meow_theme_get();
meow_theme ex; meow_theme_export(&ex);
CHECK(ex.primary == th->primary && ex.surface == th->surface &&
ex.surfaceVariant == th->surfaceVariant, "export == get (spot roles)");
meow_ui_clear(0);
meow_progress_set(prog, 40); meow_widget_draw(prog);
meow_widget_draw(prev);
meow_widget_draw(pause);
meow_widget_draw(next);
char nm[80];
int progpx = region_nonbg(NP_PROG_X, NP_PROG_Y, NP_PROG_W, 6, th->surface);
int btnpx = region_nonbg(NP_PLAY_CX - NP_PLAY_R, NP_BTN_Y - NP_PLAY_R,
NP_PLAY_R * 2, NP_PLAY_R * 2, th->surface);
snprintf(nm, sizeof nm, "style=%s mode=%s prog+btn render",
styles[si] ? "MIUIx" : "MD3", modes[mi] ? "dark" : "light");
CHECK(progpx > 0 && btnpx > 0, nm);
}
}
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}
@@ -0,0 +1,235 @@
/* test_power_menu_widgets.c — M7.0b 电源菜单控件化宿主验证。
*
* 复现 show_power_menu() 所建控件集(pm_widgets_create:meow_card + 6 圆钮,BACK/CANCEL 走
* meow_iconbtn、POWER/RESTART/UPGRADE/LOCK 走 meow_widget_custom_create 圆形画布),验证:
* P1 圆形命中:圆心命中、角部不命中(区别于矩形命中);
* P2 owner==哨兵 + enabled 双闸门(非 active / 禁用不命中);
* P3 选中/按压态像素变化(SDK 状态层,selected/pressed 改侧像素);
* P4 主题四态(MD3/MIUIx × 明暗)下卡片+圆钮渲染非背景像素 + meow_theme_export 九字段映射。
*
* 几何常量与 power_menu.c(pm_cy/pm_cx=util.c 同源)/meow_launcher_internal.h 镜像;
* 控件创建方式与 pm_widgets_create 同(owner=哨兵 0x50,back/cancel 用 MEOW_ICON_*)。
*
* 编译(在 patch_v2/meow_launcherd 下):
* gcc -Wall -Wextra -o test_power_menu_widgets test_power_menu_widgets.c \
* ../sdk/test_host_stub.c ../sdk/meow_ui_gfx.c ../sdk/meow_ui_sys.c \
* ../sdk/meow_ui_theme.c ../sdk/meow_ui_vol.c ../sdk/meow_ui_log.c \
* ../sdk/meow_ui_widget.c ../sdk/meow_ui_input.c ../sdk/meow_ui_activity.c \
* ../sdk/meow_ui_dialog.c ../sdk/meow_ui_ime.c ../sdk/meow_ui_str.c ../sdk/meow_pinyin.c \
* ../sdk/meow_ui_draw.c ../sdk/meow_ui_view.c ../sdk/meow_ui_box.c ../sdk/meow_ui_anim.c \
* ../sdk/meow_ui_scroll.c ../sdk/meow_ui_img.c ../sdk/meow_ui_media.c \
* ../sdk/meow_notify_ipc.c ../sdk/stb_image.c -I../sdk -I. -DMEOW_UI_HOST_TEST -pthread -lm
* 运行:./test_power_menu_widgets
*/
#include "meow_ui.h"
#include "meow_ui_internal.h" /* meow_ui_back_buf/pitch/bpp + meow_gfx_fill_circle + meow_widget_rect */
#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)
/* ---- 与 power_menu.c / meow_launcher_internal.h 同源几何(镜像) ---- */
#define PM_NUM 6
#define PM_R 50 /* 圆钮半径(直径 100) */
#define PM_BTN_SIZE (PM_R * 2)
#define PM_CARD_X0 40
#define PM_CARD_Y0 115
#define PM_CARD_W 400
#define PM_CARD_H 505
#define PM_CARD_R 26
#define PM_SENTINEL 0x50
/* pm_cx[] = util.c 同源;pm_cy[] = power_menu.c 同源 */
static const int pm_cx[PM_NUM] = { 175, 305, 175, 305, 175, 305 };
static const int pm_cy[PM_NUM] = { 250, 250, 390, 390, 530, 530 };
/* SDK 图标:2 回HiBy=ARROW_BACK、5 取消=CLOSE;其余 -1=自定义画布(pm_icons 专有) */
static const int pm_sdk_icon[PM_NUM] = {
-1, -1, MEOW_ICON_ARROW_BACK, -1, -1, MEOW_ICON_CLOSE
};
static meow_widget *card = NULL, *btn[PM_NUM] = {NULL};
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(int x, int y) { unsigned short p; rd_px(x, y, &p); return p; }
/* 页面底色(meow_theme surface)。区域含「非页面底色」像素计数 = 控件渲染判据。 */
static int region_nonbg(int x, int y, int w, int h, uint32_t bg)
{
unsigned short bg565 = ((bg >> 8) & 0xF800) | ((bg >> 5) & 0x07E0) | ((bg >> 3) & 0x001F);
int cnt = 0;
for (int dy = 0; dy < h; dy++)
for (int dx = 0; dx < w; dx++)
if (px565(x + dx, y + dy) != bg565) cnt++;
return cnt;
}
/* 按主题混合(镜像 power_menu.c pm_blend:base 向 top 混 pct%,24-bit 每通道)。 */
static uint32_t pm_blend(uint32_t base, uint32_t top, int pct)
{
int br = (base >> 16) & 0xFF, bg = (base >> 8) & 0xFF, bb = base & 0xFF;
int tr = (top >> 16) & 0xFF, tg = (top >> 8) & 0xFF, tb = top & 0xFF;
int r = (br * (100 - pct) + tr * pct) / 100;
int g = (bg * (100 - pct) + tg * pct) / 100;
int b = (bb * (100 - pct) + tb * pct) / 100;
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
}
/* 自定义画布(镜像 pm_render_cb:selected 圆底 + 禁用/按压自叠 + 内圆标记) */
static void cr_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
unsigned st = meow_widget_state(w);
int sel = (st & MEOW_WS_SELECTED) != 0;
int disabled = (st & MEOW_WS_ENABLED) == 0;
int pressed = (st & MEOW_WS_PRESSED) != 0;
uint32_t base = sel ? th->primaryContainer : th->surfaceContainer;
uint32_t mark = sel ? th->onPrimaryContainer : th->onSurface;
if (disabled) base = pm_blend(base, th->onSurface, 38);
else if (pressed) base = pm_blend(base, th->onSurface, sel ? 24 : 12);
meow_gfx_fill_circle(cx, cy, r, base);
meow_gfx_fill_circle(cx, cy, r / 2, mark);
}
/* 自定义画布命中(镜像 pm_hit_cb:圆形) */
static int cr_hit(meow_widget *w, int x, int y, void *ctx)
{
(void)ctx;
int rc[4]; meow_widget_rect(w, rc);
int cx = rc[0] + rc[2] / 2, cy = rc[1] + rc[3] / 2;
int r = (rc[2] < rc[3] ? rc[2] : rc[3]) / 2;
int dx = x - cx, dy = y - cy;
return dx * dx + dy * dy <= r * r;
}
static void mk_widgets(void)
{
meow_ui_activity_push(PM_SENTINEL);
card = meow_card_create(PM_CARD_X0, PM_CARD_Y0, PM_CARD_W, PM_CARD_H);
for (int i = 0; i < PM_NUM; i++) {
int x = pm_cx[i] - PM_BTN_SIZE / 2, y = pm_cy[i] - PM_BTN_SIZE / 2;
if (pm_sdk_icon[i] >= 0)
btn[i] = meow_iconbtn_create(x, y, PM_BTN_SIZE, pm_sdk_icon[i]);
else
btn[i] = meow_widget_custom_create(x, y, PM_BTN_SIZE, PM_BTN_SIZE,
cr_render, cr_hit, (void *)(intptr_t)i);
}
/* iconbtn 底色=surfaceContainer(与卡片 Low 拉开对比;镜像 pm_refresh_theme) */
const meow_theme *th = meow_theme_get();
meow_widget_opts op; memset(&op, 0, sizeof op);
op.bg = th->surfaceContainer;
for (int i = 0; i < PM_NUM; i++)
if (btn[i] && pm_sdk_icon[i] >= 0) meow_widget_set_opts(btn[i], &op);
meow_ui_activity_pop();
}
/* 重绘整卡(按钮选中全清) */
static void draw_all(int sel)
{
if (card) meow_widget_draw(card);
for (int i = 0; i < PM_NUM; i++) {
if (!btn[i]) continue;
meow_widget_set_selected(btn[i], i == sel);
meow_widget_draw(btn[i]);
}
}
int main(void)
{
setbuf(stdout, NULL);
setenv("MEOW_UI_DUMP", "/tmp/powermenu.ppm", 1);
meow_ui_opts o = { 480, 720 };
assert(meow_ui_init(&o) == 0);
/* ============ P1 圆形命中(back=iconbtn idx2, cx=175,cy=390;custom=idx0, cx=175,cy=250) ============ */
printf("P1 circular hit\n");
meow_ui_activity_push(PM_SENTINEL);
mk_widgets();
CHECK(meow_widget_hit(btn[2], 175, 390), "iconbtn center hits");
CHECK(!meow_widget_hit(btn[2], 175 - PM_BTN_SIZE / 2, 390 - PM_BTN_SIZE / 2),
"iconbtn corner (dist~70>50) MISSES");
CHECK(meow_widget_hit(btn[0], 175, 250), "custom center hits");
CHECK(!meow_widget_hit(btn[0], 175 - PM_BTN_SIZE / 2, 250 - PM_BTN_SIZE / 2),
"custom corner MISSES");
/* ============ P2 owner/enabled 双闸门 ============ */
printf("P2 owner/enabled gate\n");
meow_ui_activity_push(8); /* 非 active(owner=0x50≠8) */
CHECK(!meow_widget_hit(btn[2], 175, 390), "non-active owner -> MISS");
meow_ui_activity_pop(); /* 回 0x50 */
CHECK(meow_widget_hit(btn[2], 175, 390), "back to sentinel -> hits");
meow_widget_set_enabled(btn[2], 0);
CHECK(!meow_widget_hit(btn[2], 175, 390), "disabled -> MISS");
meow_widget_set_enabled(btn[2], 1);
CHECK(meow_widget_hit(btn[2], 175, 390), "enabled -> hits");
/* ============ P3 选中/按压态像素变化 ============ */
printf("P3 selected/pressed state (SDK state layer)\n");
meow_ui_clear(0x000000);
draw_all(-1); /* 全部非选中 */
unsigned short id0 = px565(175, 250); /* custom idx0 圆心 */
meow_widget_set_selected(btn[0], 1); meow_widget_draw(btn[0]);
unsigned short sel0 = px565(175, 250);
CHECK(sel0 != id0, "custom selected -> centre pixel changes");
meow_widget_set_selected(btn[0], 0);
meow_widget_set_state(btn[0], MEOW_WS_PRESSED, 1); meow_widget_draw(btn[0]);
unsigned short pr0 = px565(175 + 38, 250); /* 内圆 r=25 外的底色环带(按压态连底色变) */
meow_widget_set_state(btn[0], MEOW_WS_PRESSED, 0); meow_widget_draw(btn[0]);
unsigned short pr0b = px565(175 + 38, 250);
CHECK(pr0 != pr0b, "custom pressed -> bg tint pixel changes");
/* iconbtn 选中态 */
draw_all(-1);
unsigned short ic0 = px565(175 + PM_BTN_SIZE / 4, 390);
meow_widget_set_selected(btn[2], 1); meow_widget_draw(btn[2]);
unsigned short ic1 = px565(175 + PM_BTN_SIZE / 4, 390);
CHECK(ic1 != ic0, "iconbtn selected -> circle-bg pixel changes");
/* ============ P4 主题四态(MD3/MIUIx × 明暗):卡片+圆钮渲染 + export 九字段 ============ */
printf("P4 theme 4-state render + meow_theme_export\n");
const int styles[2] = { MEOW_STYLE_MD3, MEOW_STYLE_MIUIX };
const int modes[2] = { MEOW_THEME_LIGHT, MEOW_THEME_DARK };
for (int si = 0; si < 2; si++) {
for (int mi = 0; mi < 2; mi++) {
meow_ui_set_style(styles[si]);
meow_theme_set_mode((meow_theme_mode)modes[mi]);
const meow_theme *th = meow_theme_get();
meow_theme ex; meow_theme_export(&ex);
/* export == 当前解析生效主题(单一权威)+ 九字段映射到 launcher 便利结构 */
CHECK(ex.surface == th->surface &&
ex.surfaceVariant == th->surfaceVariant &&
ex.surfaceContainer == th->surfaceContainer &&
ex.primary == th->primary &&
ex.onSurface == th->onSurface &&
ex.onSurfaceVariant == th->onSurfaceVariant &&
ex.outline == th->outline &&
ex.error == th->error &&
ex.surfaceTint == th->surfaceTint,
"export == meow_theme_get (9-role)");
meow_widget_set_opts(btn[2], &(meow_widget_opts){ .bg = th->surfaceContainer });
meow_ui_clear(0); draw_all(-1);
int cardpx = region_nonbg(60, 130, 200, 40, th->surface);
int btnpx = region_nonbg(pm_cx[0] - PM_BTN_SIZE / 2, pm_cy[0] - PM_BTN_SIZE / 2,
PM_BTN_SIZE, PM_BTN_SIZE, th->surface);
char nm[80];
snprintf(nm, sizeof nm, "style=%s mode=%s card renders",
styles[si] ? "MIUIx" : "MD3", modes[mi] ? "dark" : "light");
CHECK(cardpx > 0 && btnpx > 0, nm);
}
}
meow_ui_activity_pop();
meow_ui_shutdown();
printf(g_fail ? "\nRESULT: %d FAIL(S)\n" : "\nRESULT: ALL PASS\n", g_fail);
return g_fail;
}