Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/notif_center.c
T
MeowOS 4bafdb5d69 fix(keys): 插孔消抖 + 音量 ramp 3s 上限 + 亮度写日志 — 耳机插拔 ADC 幽灵音量键修复
真机实证(meow_20260901_034946.log): 4.4 插入瞬间电气瞬态在 jz_adc_aux(event2)
产生 VOL_UP 幽灵按住, vol_ramp 24%→上限50 (225391 ramp 起→227213 爬满,
紧随 audiod jack changed). 修(限 meow_launcherd/+meow_watchdog/):
① launcher audio.c + watchdog 各自读 /sys/class/switch/{headset,balance}/state
  (节流缓存 1s/500ms, 热路径不每圈读文件), 变化后 800ms 内忽略音量键边沿
  (vol_ramp_begin 不起/在爬 ramp 中止/wd_vol_adj 单步忽略)
② ramp 总时长上限 VOL_RAMP_MAX_MS=3000 兜底防一次幽灵打满(正常长按 24→50 ~1.4s 不伤)
③ write_brightness(val,src) 全 10 调用点带源+逐写 logmsg, 亮度跳上限下次可定位
  (grep 全库: 写 backlight 仅 launcher/watchdog/svc_power/bootmenu, 无确定 255 路径)
宿主: 新 test_vol_ramp_jack 15/15 PASS(单击/长按回归+3s cap+消抖忽略/中止/恢复);
test_wd_fw 17/17 + test_settings_brightness + test_notif_tf PASS; MIPS -Wall -Wextra 0 warning
(顺带修 watchdog 既有 sys/poll.h→poll.h 重定向 warning).
watchdog 已热更设备 /usr/data(baa997ea); launcher 7be1d4eb 待固件. fw_content.conf 已更新.
遗留: SDK meow_ui_input.c 音量键路径需同款消抖(sdk/ 有主未动)
2026-09-01 04:28:10 +08:00

1588 lines
86 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* notif_center.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
static ncp_row_t g_rows[NCP_MAX_ROWS];
static int g_nrows = 0;
static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */
static int g_ncp_napps = 0;
/* §10.5:App 前台叠加会话期间,run_notif_center 周期探测该 App 是否存活(kill(pid,0))。
* >0 = 正在探测(app_launcher.c run_app_notif_overlay 置位);发现 ESRCH → g_notif_ctrl_died=1
* 立即结束会话(不 SIGCONT 死进程、不恢复死帧),让前台 waitpid 循环走统一退出路径。 */
int g_notif_ctrl_pid = 0;
int g_notif_ctrl_died = 0;
/* ⑥ 勿扰面板内开关:当前 dnd 态(开面板/切换时从 meow_settings notify.dnd 读/写,零旁路)。
* ncp_collect_cb / ncp_active_count 按它过滤行(only urgency>=NCP_DND_MIN_URGENCY when on)。 */
static int g_ncp_dnd = 0;
/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。
* g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。
* run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint)
* 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */
static int g_ncp_swipe_row = -1;
static int g_ncp_swipe_off = 0;
/* ---- 阶段D附录B:通知中心顶部媒体卡片(固定于通知列表上方,活动媒体会话时显示)---- */
#define NCP_MEDIA_H 112 /* 卡片高(封面/信息/控制/进度) */
#define NCP_MEDIA_CARD_X 12
#define NCP_MEDIA_CARD_W (fb_xres - 24)
static meow_img *g_ncp_media_cover = NULL; /* 封面缓存(按 artwork 路径变化重载,不每帧解码) */
static char g_ncp_media_cover_path[200] = "";
/* 卡片顶缘(恒定 = 通知列表区上缘)。 */
static int ncp_media_y(void) { return NCP_LIST_TOP; }
/* 卡片是否显示(活动媒体会话存在且非停止)。 */
static int ncp_media_on(void) { return meow_media_has_source() ? NCP_MEDIA_H : 0; }
/* 通知列表可视区实际顶缘(含媒体卡片时下移一个卡片高)——绘制/命中统一用,避免硬编码 NCP_LIST_TOP。 */
static int ncp_list_top(void) { return NCP_LIST_TOP + ncp_media_on(); }
/* 列表可视区高(含媒体卡片时缩短)。 */
static int ncp_list_view(void) { int v = NCP_LIST_BOT - ncp_list_top(); return v > 0 ? v : 0; }
/* 卡片内按钮几何(供绘制与命中共用)。 */
#define NCP_MEDIA_BTN_Y 34 /* 相对卡片顶的按钮中心 y */
#define NCP_MEDIA_PREV_CX 300
#define NCP_MEDIA_PLAY_CX 372
#define NCP_MEDIA_NEXT_CX 444
#define NCP_MEDIA_BTN_R 22 /* play 半径;prev/next 略小 */
#define NCP_MEDIA_PROG_Y 74 /* 相对卡片顶的进度条 top */
/* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */
static void ncp_strncpy(char *dst, const char *src, size_t len)
{
if (!dst || len == 0) return;
if (!src) { dst[0] = 0; return; }
strncpy(dst, src, len - 1);
dst[len - 1] = 0;
}
/* app_id → 显示名(apps[] 匹配则用 name,否则原样 app_id;无则空串) */
static const char *ncp_app_name(const char *app_id)
{
if (app_id && g_ncp_apps)
for (int i = 0; i < g_ncp_napps; i++)
if (!strcmp(g_ncp_apps[i].id, app_id)) return g_ncp_apps[i].name;
return app_id ? app_id : "";
}
/* 通知图标加载回调:把 app_icon(相对卡根/绝对)解成绝对路径,加载 meow_img。 */
static void ncp_load_icon(ncp_row_t *r)
{
if (!r) return;
r->img = NULL;
if (!r->icon[0]) return;
char path[MEOW_NOTIF_ICON_MAX + 80];
if (r->icon[0] == '/') snprintf(path, sizeof path, "%s", r->icon);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", r->icon);
r->img = meow_img_load_file(path);
}
static int ncp_collect_cb(const meow_notif_t *n, void *ctx)
{
(void)ctx;
if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
/* 勿扰(⑥):dnd 开时只收集 urgency>=NCP_DND_MIN_URGENCY(HIGH/CRITICAL)。 */
if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0;
ncp_row_t *r = &g_rows[g_nrows++];
memset(r, 0, sizeof *r);
r->id = n->id;
ncp_strncpy(r->app_id, n->app_id, sizeof r->app_id);
ncp_strncpy(r->category, n->category, sizeof r->category);
ncp_strncpy(r->summary, n->summary, sizeof r->summary);
ncp_strncpy(r->body, n->body, sizeof r->body);
ncp_strncpy(r->icon, n->app_icon, sizeof r->icon);
ncp_strncpy(r->target, n->target_activity, sizeof r->target);
r->created_ms = n->created_ms;
r->progress = n->progress;
r->urgency = n->urgency;
ncp_load_icon(r);
return 0;
}
/* 释放全部缓存图标 + 清行数(面板关闭/列表刷新前用)。 */
static void ncp_rows_free(void)
{
for (int i = 0; i < g_nrows; i++)
if (g_rows[i].img) { meow_img_free(g_rows[i].img); g_rows[i].img = NULL; }
g_nrows = 0;
}
/* 重新采集通知列表(比 meow_notif_count 变化时调用)。 */
static int ncp_refresh_rows(void)
{
ncp_rows_free();
meow_notif_list(NULL, ncp_collect_cb, NULL);
return g_nrows;
}
/* 当前应显示的活跃通知数(按 g_ncp_dnd 过滤,与 g_rows 同口径)——供变化检测/重采集用。 */
static int ncp_count_cb(const meow_notif_t *n, void *ctx)
{
if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0;
(*(int *)ctx)++;
return 0;
}
static int ncp_active_count(void)
{
int c = 0;
meow_notif_list(NULL, ncp_count_cb, &c);
if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS;
return c;
}
/* 通知列表是否变化(按过滤后活跃数比;新条/撤下/TTL 释放都反映在 count,含 dnd 过滤)。 */
static int ncp_rows_changed(void)
{
return ncp_active_count() != g_nrows;
}
/* ---- 开关平移动画辅助(§9:快照 blit + 平移;只在 run_notif_center 内分配/释放)----
* g_ncp_ph = 面板高 PH = fb_yres - NCP_TOP;两块快照各 PH × fb_line 字节:
* bg_snap = 进入面板时(绘制面板前)从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拍的背景(应用列表);
* panel_snap= 最终位置整面板渲染(draw_notif_center 后拍),稳态重绘后刷新。
* 快照不可用(分配失败/无 fb)时动画降级为瞬时显示/消失(fallback)。 */
static unsigned char *g_ncp_bg_snap = NULL;
static unsigned char *g_ncp_panel_snap = NULL;
static int g_ncp_ph = 0;
static void ncp_snap_init(void)
{
int ph = fb_yres - NCP_TOP;
if (ph <= 0 || fb_line <= 0) { g_ncp_ph = 0; return; }
size_t bsz = (size_t)ph * (size_t)fb_line;
g_ncp_bg_snap = malloc(bsz);
g_ncp_panel_snap = malloc(bsz);
if (!g_ncp_bg_snap || !g_ncp_panel_snap) {
free(g_ncp_bg_snap); g_ncp_bg_snap = NULL;
free(g_ncp_panel_snap); g_ncp_panel_snap = NULL;
g_ncp_ph = 0;
return;
}
g_ncp_ph = ph;
}
static void ncp_snap_free(void)
{
free(g_ncp_bg_snap); g_ncp_bg_snap = NULL;
free(g_ncp_panel_snap); g_ncp_panel_snap = NULL;
g_ncp_ph = 0;
}
/* 从 back_buf 行 [NCP_TOP, NCP_TOP+PH) 拷贝到 dst(行宽 fb_line)。 */
static void ncp_snap_copy_rows(unsigned char *dst, const unsigned char *base)
{
if (!dst || g_ncp_ph <= 0) return;
size_t row = (size_t)fb_line;
for (int i = 0; i < g_ncp_ph; i++)
memcpy(dst + (size_t)i * row, base + (size_t)i * row, row);
}
/* 拍背景(应用列表):进入面板时、面板绘制前调用。 */
static void ncp_snap_bg(void)
{
ncp_snap_copy_rows(g_ncp_bg_snap, fb + (size_t)NCP_TOP * fb_line);
}
/* 拍最终整面板:draw_notif_center 画完后 / 每次稳态重绘后调用。 */
static void ncp_snap_panel(void)
{
ncp_snap_copy_rows(g_ncp_panel_snap, fb + (size_t)NCP_TOP * fb_line);
}
/* 重建动画帧:把 back_buf 面板区 [NCP_TOP..) 还原为「背景 + 平移后的面板」。
* off∈[-PH,0];prev_bot=上一帧覆盖底缘(exclusive),用于关(收窄)时恢复新露出背景条。
* 返回本帧覆盖底缘(exclusive,供调用方算脏区与传给下次)。 */
static int ncp_anim_rebuild(int off, int prev_bot)
{
if (!g_ncp_bg_snap || !g_ncp_panel_snap || g_ncp_ph <= 0) return NCP_TOP;
int top = NCP_TOP;
int ph = g_ncp_ph;
int cur_bot = mnp_anim_covered_bot(top, ph, off); /* ∈[top, top+ph](§9.3 裁切) */
if (cur_bot < top) cur_bot = top;
if (cur_bot > top + ph) cur_bot = top + ph;
size_t row = (size_t)fb_line;
/* 1) 面板收窄(收回中):[cur_bot, prev_bot) 新露出 → 恢复背景(应用列表)。 */
if (prev_bot > cur_bot) {
int b0 = prev_bot < top + ph ? prev_bot : top + ph;
for (int sy = cur_bot; sy < b0; sy++)
memcpy(fb + (size_t)sy * row,
g_ncp_bg_snap + (size_t)(sy - top) * row, row);
}
/* 2) 贴平移面板:[top, cur_bot) 从 panel_snap 行 yy = sy-(top+off) 拷回。 */
for (int sy = top; sy < cur_bot; sy++) {
int yy = sy - (top + off); /* ∈[0, ph)(见 §9.3 裁切论证) */
if (yy < 0) yy = 0;
if (yy >= ph) yy = ph - 1;
memcpy(fb + (size_t)sy * row,
g_ncp_panel_snap + (size_t)yy * row, row);
}
return cur_bot;
}
/* ⑨ 应用一帧面板动画(开关时间轴 §9 与跟手拖动 ⑨ 共用):把 back_buf 面板区重建为
* 「背景 + 平移后的面板」并标脏/呈现。p=当前进度;prev_bot=上一帧覆盖底缘(exclusive,
* 回填本帧);ok=快照可用(否则 no-op,动画降级为瞬时)。只刷面板矩形 + 新露出条,
* 顶= NCP_TOP(不盖状态栏)、不含 [0,NCP_TOP) 带;不重排面板控件(panel_snap 固定)。 */
static void ncp_anim_frame(int p, int *prev_bot, int ok)
{
if (!ok || g_ncp_ph <= 0 || !prev_bot) return;
int off = mnp_anim_off(p, NCP_ANIM_FIX, g_ncp_ph);
int cur_bot = ncp_anim_rebuild(off, *prev_bot);
int maxb = (*prev_bot > cur_bot) ? *prev_bot : cur_bot;
*prev_bot = cur_bot;
meow_dirty_add(0, NCP_TOP, fb_xres, maxb - NCP_TOP);
meow_ui_flush();
}
/* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */
static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw)
{
if (!s) s = "";
if (ui_width(s, size) <= maxw) { ui_draw(x, y, s, color, size); return; }
int ell = ui_width("...", size);
int limit = maxw - ell;
int len = 0;
char buf[128];
while (s[len] && len < (int)sizeof buf - 8) {
/* 取当前字节对应的 UTF-8 码长(1-4),避免切断多字节字符 */
unsigned char c0 = (unsigned char)s[len];
int cp = 1;
if ((c0 & 0xE0) == 0xC0) cp = 2;
else if ((c0 & 0xF0) == 0xE0) cp = 3;
else if ((c0 & 0xF8) == 0xF0) cp = 4;
if (len + cp >= (int)sizeof buf - 1) break;
for (int k = 0; k < cp; k++) buf[len + k] = s[len + k];
buf[len + cp] = 0;
if (ui_width(buf, size) > limit) { buf[len] = 0; break; }
len += cp;
}
if (len > 0) {
if (len + 3 < (int)sizeof buf) { buf[len] = '.'; buf[len+1] = '.'; buf[len+2] = '.'; buf[len+3] = 0; }
ui_draw(x, y, buf, color, size);
} else {
ui_draw(x, y, "...", color, size);
}
}
/* 画一个小圆点(近似圆 = 圆角方 r=radius;主题 chip 用)。 */
static void ncp_dot(int cx, int cy, int r, uint32_t col)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, col);
}
/* 画 32×32 图标(复用 sb_blit_icon;ink 垂直中心对齐 cy)。 */
static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h)
{
int saved = g_sb_icon_cy;
g_sb_icon_cy = cy;
sb_blit_icon(icon, slot_x, col, ink_h);
g_sb_icon_cy = saved;
}
/* chip 位置(4 个,居中排布)。返回下标对应 chip 的 x 左缘。 */
static int ncp_chip_x(int chip)
{
int total = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
int x0 = (fb_xres - total) / 2;
return x0 + chip * (NCP_CHIP_W + NCP_CHIP_GAP);
}
static int ncp_chip_hit(int chip, int tx, int ty)
{
int x = ncp_chip_x(chip);
return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H);
}
/* 面板当前 4 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */
static void ncp_chip_state(int chip, int *on, char *val, int vlen)
{
*on = 0;
if (val && vlen > 0) val[0] = 0;
if (chip == NCP_CHIP_WIFI) {
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
*on = v;
} else if (chip == NCP_CHIP_BT) {
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
*on = v;
} else if (chip == NCP_CHIP_THEME) {
int v = 0; meow_settings_get_enum("display.theme", &v); /* dark=0, light=1 */
*on = (v == 1);
if (val && vlen > 0) snprintf(val, vlen, "%s", *on ? "浅色" : "深色");
} else if (chip == NCP_CHIP_BRIGHT) {
int v = 0; meow_settings_get_int("display.brightness", &v);
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
int vc = mnp_brightness_clamp(v, vmin);
if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc);
*on = (vc >= 50); /* 过半视为"亮"态 */
} else if (chip == NCP_CHIP_POWER) {
/* 电源菜单 chip:非开关(无 on/off 态),恒示中性;点击由 ncp_chip_apply 唤起电源菜单。 */
*on = 0;
}
}
/* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。
* 返回 1=主题变化(需重刷状态栏);2=电源菜单已弹出(调用方置 after_modal=1 整帧重绘还原);
* 0=其它。电源菜单 chip 复用既有系统级 show_power_menu(含 dim/圆钮/音量导航/电源确认/8s强关/
* 空闲自动息屏),真弹对话框,动作在函数内 system();取消则返回续 panel —— 与电源键长按同款。 */
static int ncp_chip_apply(int chip)
{
if (chip == NCP_CHIP_WIFI) {
int v = 0; meow_settings_get_switch("network.wifi_enabled", &v);
meow_settings_set_switch("network.wifi_enabled", !v);
s_net_toggle_wifi(!v);
s_net_refresh();
} else if (chip == NCP_CHIP_BT) {
int v = 0; meow_settings_get_switch("network.bluetooth_enabled", &v);
meow_settings_set_switch("network.bluetooth_enabled", !v);
s_net_toggle_bt(!v);
s_net_refresh();
} else if (chip == NCP_CHIP_THEME) {
/* 快捷主题:切到 manual(确保生效),再翻转 dark/light(display.theme)。 */
int mode = THEME_MODE_MANUAL;
meow_settings_get_enum("display.theme_mode", &mode);
if (mode != THEME_MODE_MANUAL) meow_settings_set_enum("display.theme_mode", THEME_MODE_MANUAL);
int v = 0; meow_settings_get_enum("display.theme", &v);
meow_settings_set_enum("display.theme", v ? 0 : 1);
paint_sb_right(); /* 状态栏文字/底色随主题刷新 */
meow_settings_save();
return 1; /* 主题变化 → 重刷状态栏 */
} else if (chip == NCP_CHIP_BRIGHT) {
/* 快捷亮度:在「极暗(下限)」与「100」之间切换。 */
int cur = 0; meow_settings_get_int("display.brightness", &cur);
int vmin = 1; meow_settings_get_int("display.brightness_min", &vmin);
if (mnp_brightness_clamp(cur, vmin) < 100) {
meow_settings_set_int("display.brightness", 100);
write_brightness(100, "notif_center bright_chip");
g_sys.brightness = 100;
} else {
meow_settings_set_int("display.brightness", vmin);
write_brightness(vmin, "notif_center bright_chip");
g_sys.brightness = vmin;
}
paint_sb_right();
} else if (chip == NCP_CHIP_POWER) {
/* 电源菜单 chip:唤起既有系统级电源菜单(真弹对话框,动作在函数内 system();取消则返回)。
* 不涉及任何设置项(电源菜单不走 meow_settings,零旁路)。返回 2 → 调用方整帧重绘还原面板/状态栏。 */
logmsg("notif-center: power chip -> show power menu");
show_power_menu();
return 2;
}
meow_settings_save();
return 0;
}
/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。
* off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */
static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th)
{
if (off >= 0) return;
int bw = -off;
if (bw <= 0) return;
int bx = NCP_ROW_X + NCP_ROW_W + off; /* 被空出的右条左缘 */
fill_rect(bx, ry, bw, NCP_ROW_H - 6, th->error);
if (bw >= 44) {
int cx = bx + bw / 2, cy = ry + (NCP_ROW_H - 6) / 2;
ui_draw(cx - ui_width("删除", 16) / 2, cy - meow_font_line_height(16) / 2,
"删除", 0xFFFFFF, 16);
}
}
/* 画一行内容(行卡 + 图标/APP名/时间/标题/正文/进度),整体随 off 左移(⑦ 行左滑跟手)。
* i=g_rows 下标;ry=行顶(已含 scroll);off=行左移量(px,<=0)。 */
static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th)
{
const ncp_row_t *r = &g_rows[i];
meow_gfx_fill_round(NCP_ROW_X + off, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container);
/* 图标 */
int icx = NCP_ICON_X + off, icy = ry + (NCP_ROW_H - 32) / 2;
if (r->img) meow_ui_img_cover(icx, icy, 32, r->img);
else {
/* 回退:主题色圆点 + 首字符 */
const char *nm = ncp_app_name(r->app_id);
ncp_dot(icx + 16, icy + 16, 16, th->primary);
char ch = nm[0] ? nm[0] : '?';
char cbuf[4] = { ch, 0, 0, 0 };
/* 首字符若是多字节(中文),取整码 */
if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0;
if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } }
int cw = ui_width(cbuf, 20);
ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20);
}
/* app 名 + 时间 */
ui_draw(NCP_TEXT_X + off, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16);
char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf);
ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16) + off, ry + 12, tbuf, th->on_surface_variant, 16);
/* 标题 + 内容 */
ncp_text(NCP_TEXT_X + off, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W);
ncp_text(NCP_TEXT_X + off, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W);
/* 进度(progress>=0):细条 */
if (r->progress >= 0) {
int px = NCP_TEXT_X + off, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14;
fill_rect(px, py, pw, 4, th->surface_variant);
int fw = r->progress * pw / 100;
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
}
}
/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。
* ⑦:左滑中的行(g_ncp_swipe_row)按 g_ncp_swipe_off 左移,其下先画删除底(ncp_draw_delete_bg)。 */
static void ncp_draw_list(int scroll)
{
const meow_ui_theme_t *th = s_ui_theme();
int lt = ncp_list_top(), lv = ncp_list_view();
if (g_nrows == 0) {
ui_draw(NCP_ROW_X + 8, lt + 40, "暂无通知", th->on_surface_variant, 22);
return;
}
int total = g_nrows * NCP_ROW_H;
scroll = mnp_scroll_clamp(scroll, total, lv);
for (int i = 0; i < g_nrows; i++) {
int ry = lt + i * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */
int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */
if (off < 0) ncp_draw_delete_bg(ry, off, th);
ncp_draw_row(i, ry, off, th);
}
/* 滚动条 */
if (total > lv) {
int thumb_h = lv * lv / total; if (thumb_h < 24) thumb_h = 24;
int ty = lt + (lv - thumb_h) * scroll / (total - lv);
meow_gfx_fill_round(fb_xres - 8, lt, 5, lv, 3, th->surface_container);
meow_gfx_fill_round(fb_xres - 8, ty, 5, thumb_h, 3, th->on_surface_variant);
}
}
/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底(面板 surface_variant),
* 再画可见行 + 滚动条;只标列表带脏([NCP_LIST_TOP, NCP_LIST_BOT)),面板体其它控件 / 状态栏带
* [0,NCP_TOP) 均不触碰 —— 满足「滚动只重绘列表可视区,绝不整屏」。 */
static void ncp_repaint_list(int scroll)
{
const meow_ui_theme_t *th = s_ui_theme();
int lt = ncp_list_top(), lv = ncp_list_view();
fill_rect(0, lt, fb_xres, lv, th->surface_variant);
ncp_draw_list(scroll);
meow_dirty_add(0, lt, fb_xres, lv);
meow_ui_flush();
}
/* ⑦ 仅重绘一行(行左滑跟手/回弹/滑出专用;只刷该行矩形,不整面板、不刷状态栏)。
* 清该行带(含被滑出的右条)→ 画删除底(若 off<0)+ 该行内容位移 → 只标该行带脏。 */
static void ncp_repaint_row(int scroll, int row, int off)
{
const meow_ui_theme_t *th = s_ui_theme();
if (row < 0 || row >= g_nrows) return;
int lt = ncp_list_top();
int ry = lt + row * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < lt || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */
/* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */
fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant);
if (off < 0) ncp_draw_delete_bg(ry, off, th);
ncp_draw_row(row, ry, off, th);
meow_dirty_add(0, ry, fb_xres, NCP_ROW_H);
meow_ui_flush();
}
/* ⑥/⑧ 勿扰开关带命中(面板内小 toggle,⑧ 收窄为左半段):
* 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [0, NCP_DND_SPLIT_X) → 勿扰命中。
* 右半段归「一键清除」(ncp_clear_hit,互斥);不与 5 chip(y≤130)、滑条带(y≤238)、
* 通知列表(y≥NCP_LIST_TOP=308)、状态栏(y<28)重叠。 */
static int ncp_dnd_hit(int tx, int ty)
{
return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H &&
tx >= 0 && tx < NCP_DND_SPLIT_X);
}
/* ⑧ 一键清除带命中(勿扰对称处的右半段):
* 落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 且 x ∈ [NCP_DND_SPLIT_X, fb_xres) → 清除命中。 */
static int ncp_clear_hit(int tx, int ty)
{
return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H &&
tx >= NCP_DND_SPLIT_X && tx < fb_xres);
}
/* ⑥ 绘勿扰带(⑧ 收窄为左半段):label「勿扰」+ 铃铛图标 + 状态「开/关」 + 左半段右侧 s_draw_switch(48×28)。
* 右半段由 ncp_draw_clear 画「一键清除」按钮(对称)。 */
static void ncp_draw_dnd(const meow_ui_theme_t *th)
{
int y = NCP_DND_Y;
int dnd_on = g_ncp_dnd;
int cy = y + NCP_DND_H / 2;
/* 铃铛(简化:圆顶 + 顶蒂 + 底部小圆点)。 */
int bx = NCP_ROW_X + 4, bcy = cy - 2;
meow_gfx_fill_round(bx, bcy - 7, 16, 13, 7, th->on_surface_variant); /* 铃身圆顶 */
meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */
ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */
/* label + 状态 */
ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18);
char st[16];
snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关");
int lw = ui_width("勿扰", 18);
ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st,
dnd_on ? th->primary : th->on_surface_variant, 18);
/* 左半段右侧开关(⑧:从原 fb_xres-24-48 左移到分割线左侧,48×28 垂直居中) */
s_draw_switch(NCP_DND_SPLIT_X - 6 - 48, cy - 14, dnd_on, th);
}
/* ⑧ 绘一键清除按钮(勿扰对称处的右半段):圆角按钮底 + 垃圾桶字形 + 「清除」文字。 */
static void ncp_draw_clear(const meow_ui_theme_t *th)
{
int y = NCP_DND_Y;
int cy = y + NCP_DND_H / 2;
int cx0 = NCP_DND_SPLIT_X + 8; /* 右半段按钮左缘 */
int cw = (fb_xres - NCP_ROW_X) - cx0; /* 右半段按钮宽 */
if (cw <= 0) return;
meow_gfx_fill_round(cx0, y + 4, cw, NCP_DND_H - 8, 12, th->surface_container);
/* 垃圾桶字形(简化:顶盖横条 + 桶身圆角 + 侧提手)+「清除」文字,整体在按钮内居中(美观对称)。 */
const char *lbl = "清除";
int lw = ui_width(lbl, 18);
int unit = 22; /* 图标占位宽 + 与文字的间距 */
int total = unit + lw;
int gx = cx0 + (cw - total) / 2; /* 图标左缘(居中) */
int icy = cy;
fill_rect(gx, icy - 7, 14, 2, th->on_surface_variant); /* 顶盖 */
meow_gfx_fill_round(gx + 1, icy - 3, 12, 12, 2, th->on_surface_variant);/* 桶身 */
meow_gfx_fill_round(gx - 2, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手左 */
meow_gfx_fill_round(gx + 14, icy - 6, 4, 3, 1, th->on_surface_variant);/* 侧提手右 */
ui_draw(gx + unit, cy - meow_font_line_height(18) / 2 + 2, lbl, th->on_surface, 18);
}
static int ncp_clear_collect_cb(const meow_notif_t *n, void *ctx)
{
ncp_clear_ctx_t *c = ctx;
if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; /* 保留红线项 */
if (c->n >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */
c->ids[c->n++] = n->id;
return 0;
}
/* 收集可清除通知 id 后逐条 dismiss(dismiss = archive_one(dump→TF) + 释放槽 = 转储留档 + 清出显示)。
* 返回清除条数。 */
static int ncp_clear_all(void)
{
ncp_clear_ctx_t c;
memset(&c, 0, sizeof c);
meow_notif_list(NULL, ncp_clear_collect_cb, &c);
for (int i = 0; i < c.n; i++) meow_notif_dismiss(c.ids[i]);
return c.n;
}
/* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。
* 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过,
* 使脏区仅限面板体,绝不整屏重绘。 */
/* ================= 阶段D附录B:顶部媒体卡片(绘制/命中) ================= */
/* 封面缓存:artwork 路径变化才重载(不每帧解码)。 */
static void ncp_media_cover_load(const meow_media_session_t *s)
{
if (!s) return;
if (strcmp(g_ncp_media_cover_path, s->artwork) == 0) return;
if (g_ncp_media_cover) { meow_img_free(g_ncp_media_cover); g_ncp_media_cover = NULL; }
ncp_strncpy(g_ncp_media_cover_path, s->artwork, sizeof g_ncp_media_cover_path);
if (!s->artwork[0]) return;
char path[200];
if (s->artwork[0] == '/') snprintf(path, sizeof path, "%s", s->artwork);
else snprintf(path, sizeof path, "/usr/data/mnt/sd_0/%s", s->artwork);
g_ncp_media_cover = meow_img_load_file(path);
}
/* 实心三角(dir=+1 右向 / -1 左向);size=最大宽度 px。按扫描线 fill_rect 近似 */
static void ncp_media_tri(int cx, int cy, int size, int dir, uint32_t col)
{
int half = size / 2;
for (int dy = -half; dy <= half; dy++) {
int w = half + 1 - (dy < 0 ? -dy : dy); /* 顶宽 1 → 底宽 half+1 */
if (w <= 0) continue;
int x0 = cx + (dir > 0 ? -w / 2 : w / 2 - w);
fill_rect(x0, cy + dy, w, 1, col);
}
}
/* 圆形按钮底 + 字形(kind:0=prev 1=play 2=next 3=pause 4=暂停态play)。 */
static void ncp_media_btn(int cx, int cy, int r, int kind, const meow_ui_theme_t *th)
{
meow_gfx_fill_round(cx - r, cy - r, r * 2, r * 2, r, th->primary);
uint32_t fg = 0xFFFFFF;
if (kind == 0) { /* prev:左三角 + 左竖条 */
ncp_media_tri(cx + 4, cy, r - 4, -1, fg);
fill_rect(cx - 3, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 1) { /* play:右三角 */
ncp_media_tri(cx + 2, cy, r - 2, 1, fg);
} else if (kind == 2) { /* next:右三角 + 右竖条 */
ncp_media_tri(cx - 4, cy, r - 4, 1, fg);
fill_rect(cx + 1, cy - (r - 5), 3, (r - 5) * 2, fg);
} else if (kind == 3) { /* pause:双竖条 */
int bw = (r - 6) / 2; if (bw < 2) bw = 2;
fill_rect(cx - 7, cy - (r - 5), bw, (r - 5) * 2, fg);
fill_rect(cx + 7 - bw, cy - (r - 5), bw, (r - 5) * 2, fg);
}
}
/* 画顶部媒体卡片(封面小图 + 标题/歌手 + 播放/暂停/上下曲 + 进度条)。 */
static void ncp_draw_media_card(const meow_ui_theme_t *th)
{
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return;
int cy = ncp_media_y();
meow_gfx_fill_round(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H, 14, th->surface_container);
ncp_media_cover_load(s);
int cover_s = 88, cover_x = NCP_MEDIA_CARD_X + 12, cover_y = cy + 12;
if (g_ncp_media_cover) {
meow_gfx_fill_round(cover_x, cover_y, cover_s, cover_s, 12, th->surface_variant);
meow_ui_img_cover(cover_x + 4, cover_y + 4, cover_s - 8, g_ncp_media_cover);
} else {
meow_gfx_fill_round(cover_x, cover_y, cover_s, cover_s, 12, th->primary);
const char *t = s->title[0] ? s->title : s->app_id;
int cw = ui_width(t, 20);
ui_draw(cover_x + (cover_s - cw) / 2, cover_y + (cover_s - meow_font_line_height(20)) / 2,
t, 0xFFFFFF, 20);
}
int tx = cover_x + cover_s + 14;
int tw = (NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R - 8) - tx; /* 文本宽限到上一首按钮左缘前 8px */
if (tw < 0) tw = 0;
ncp_text(tx, cy + 14, s->title, 18, th->on_surface, tw);
ncp_text(tx, cy + 40, s->artist[0] ? s->artist : s->album, 16, th->on_surface_variant, tw);
/* 控制按钮 */
int by = cy + NCP_MEDIA_BTN_Y;
ncp_media_btn(NCP_MEDIA_PREV_CX, by, NCP_MEDIA_BTN_R - 4, 0, th);
int pk = (s->state == MEOW_MEDIA_PLAYING) ? 3 /* pause */ : 1 /* play */;
ncp_media_btn(NCP_MEDIA_PLAY_CX, by, NCP_MEDIA_BTN_R, pk, th);
ncp_media_btn(NCP_MEDIA_NEXT_CX, by, NCP_MEDIA_BTN_R - 4, 2, th);
/* 进度条 + 时间 */
uint32_t pos = meow_media_display_pos(s);
int px = tx, pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px, py = cy + NCP_MEDIA_PROG_Y;
if (pw > 0) {
fill_rect(px, py, pw, 4, th->outline);
int fw = (s->dur_ms > 0) ? (int)((uint64_t)pos * pw / s->dur_ms) : 0;
if (fw > pw) fw = pw;
if (fw > 0) fill_rect(px, py, fw, 4, th->primary);
}
}
/* 卡片命中:按钮/进度(含 seek)。返回 1=已处理(内部已下发 PLAYCTL);0=未命中。 */
static int ncp_media_card_tap(int tx, int ty)
{
const meow_media_session_t *s = meow_media_current();
if (!s || s->state == MEOW_MEDIA_STOPPED) return 0;
int cy = ncp_media_y();
if (ty < cy || ty >= cy + NCP_MEDIA_H) return 0;
int by = cy + NCP_MEDIA_BTN_Y;
int cmd = 0; int64_t seek = 0;
if (tx >= NCP_MEDIA_PREV_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_PREV_CX + NCP_MEDIA_BTN_R &&
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R) cmd = 3; /* 上一首 */
else if (tx >= NCP_MEDIA_PLAY_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_PLAY_CX + NCP_MEDIA_BTN_R &&
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R)
cmd = (s->state == MEOW_MEDIA_PLAYING) ? 2u : 1u; /* 播放/暂停 */
else if (tx >= NCP_MEDIA_NEXT_CX - NCP_MEDIA_BTN_R && tx <= NCP_MEDIA_NEXT_CX + NCP_MEDIA_BTN_R &&
ty >= by - NCP_MEDIA_BTN_R && ty <= by + NCP_MEDIA_BTN_R) cmd = 4; /* 下一首 */
else if (s->dur_ms > 0) { /* 进度条 seek */
int px = NCP_MEDIA_CARD_X + 12 + 88 + 14;
int pw = NCP_MEDIA_CARD_X + NCP_MEDIA_CARD_W - 12 - px;
int py = cy + NCP_MEDIA_PROG_Y;
if (pw > 0 && ty >= py - 12 && ty <= py + 16 && tx >= px && tx <= px + pw) {
int frac = (tx - px) * 1000 / pw;
seek = (int64_t)s->dur_ms * frac / 1000;
cmd = 5;
}
}
if (!cmd) return 0;
int rc = meow_media_playctl(cmd, seek);
logmsg("notif-card: media tap cmd=%u seek=%lld rc=%d", cmd, (long long)seek, rc);
return 1;
}
/* 仅重绘媒体卡片带(进度/封面/按钮;不整面板、不刷状态栏)——局部刷新用。 */
static void ncp_repaint_media_card(void)
{
const meow_ui_theme_t *th = s_ui_theme();
if (!meow_media_has_source()) return;
int cy = ncp_media_y();
fill_rect(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H, th->surface_variant);
ncp_draw_media_card(th);
meow_dirty_add(NCP_MEDIA_CARD_X, cy, NCP_MEDIA_CARD_W, NCP_MEDIA_H);
meow_ui_flush();
}
static void draw_notif_center(int scroll, int redraw_sb)
{
const meow_ui_theme_t *th = s_ui_theme();
int W = fb_xres, H = fb_yres;
/* 状态栏带 [0,NCP_TOP) 仅当 redraw_sb=1(打开/主题变化)时清底,随后由下方 draw_statusbar()
* 覆盖重绘(Stage-One:draw_statusbar 只喂 SDK + 标脏顶条,实际由 SDK present() 重绘);
* redraw_sb=0(交互重绘,如拖滑条)时**绝不触碰**——保留 back_buf 中 SDK 状态栏/上次 top 脏,
* 避免被清成 surface 底仍被 blit → 状态栏数值闪烁/消失(真机①修复)。
* 面板体(状态栏之下 surface_variant)态恒整片铺。 */
if (redraw_sb) fill_rect(0, 0, W, NCP_TOP, th->surface);
fill_rect(0, NCP_TOP, W, H - NCP_TOP, th->surface_variant);
/* 把手 */
int grab_w = 44, grab_x = (W - grab_w) / 2;
meow_gfx_fill_round(grab_x, NCP_GRAB_Y, grab_w, 6, 3, th->outline);
/* 标题 + 「上滑/点空白关闭」 提示 */
ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22);
ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16);
/* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */
static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" };
int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP;
int chip_x0 = (W - total_chip) / 2;
for (int c = 0; c < NCP_CHIP_N; c++) {
int cx = chip_x0 + c * (NCP_CHIP_W + NCP_CHIP_GAP);
int on = 0; char val[16] = ""; ncp_chip_state(c, &on, val, sizeof val);
uint32_t bg = on ? th->primary : th->surface_container;
uint32_t fg = on ? 0xFFFFFF : th->on_surface_variant;
meow_gfx_fill_round(cx, NCP_CHIP_Y, NCP_CHIP_W, NCP_CHIP_H, 12, bg);
int icon_cx = cx + (NCP_CHIP_W - SB_ICON_SIZE) / 2;
int icon_cy = NCP_CHIP_Y + 14;
if (c == NCP_CHIP_WIFI) ncp_sb_icon(on ? SB_I_WIFI : SB_I_WIFI_OFF, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_BT) ncp_sb_icon(on ? SB_I_BT : SB_I_BT_DIS, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_THEME) {
if (on) ncp_dot(icon_cx + 16, icon_cy, 11, fg);
else { ncp_dot(icon_cx + 16, icon_cy, 11, fg); ncp_dot(icon_cx + 16, icon_cy, 6, bg); }
}
else if (c == NCP_CHIP_BRIGHT) ncp_sb_icon(SB_I_BRIGHT, icon_cx, icon_cy, fg, 22);
else if (c == NCP_CHIP_POWER) {
/* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */
int pc = icon_cx + SB_ICON_SIZE / 2;
meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */
meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */
meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */
}
char label[24];
snprintf(label, sizeof label, "%s", chip_lbl[c]);
if (val[0] && c == NCP_CHIP_BRIGHT)
snprintf(label, sizeof label, "%.9s", val); /* chip 下示值 */
ui_draw(cx + (NCP_CHIP_W - ui_width(label, 14)) / 2, NCP_CHIP_Y + NCP_CHIP_H - 18,
label, fg, 14);
}
/* 亮度滑条 */
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int bval = 0; meow_settings_get_int("display.brightness", &bval);
bval = mnp_brightness_clamp(bval, bmin);
ui_draw(NCP_SLIDER_X, NCP_SL1_Y - 26, "亮度", th->on_surface, 18);
char vbb[16]; snprintf(vbb, sizeof vbb, "%d%%", bval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbb, 18), NCP_SL1_Y - 26, vbb,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL1_Y, NCP_SLIDER_W, bval, bmin, 100, th);
/* 音量滑条 */
int vval = 0; meow_settings_get_int("audio.volume", &vval);
vval = vol_clamp(vval);
ui_draw(NCP_SLIDER_X, NCP_SL2_Y - 26, "音量", th->on_surface, 18);
char vbl[16]; snprintf(vbl, sizeof vbl, "%d%%", vval);
ui_draw(NCP_SLIDER_X + NCP_SLIDER_W - ui_width(vbl, 18), NCP_SL2_Y - 26, vbl,
th->on_surface_variant, 18);
s_draw_slider(NCP_SLIDER_X, NCP_SL2_Y, NCP_SLIDER_W, vval, 0, vol_clamp(100), th);
/* 分隔线 */
fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline);
/* ⑥ 勿扰面板内开关带(list 上方,左半段) */
ncp_draw_dnd(th);
/* ⑧ 一键清除按钮(勿扰对称处的右半段) */
ncp_draw_clear(th);
/* 阶段D附录B:顶部媒体卡片(活动媒体会话时显示,固定于列表上方;列表可视区随之下移) */
ncp_draw_media_card(th);
/* 通知列表 */
ncp_draw_list(scroll);
/* 底部提示 */
ui_draw((W - ui_width("上滑或点空白关闭", 16)) / 2, NCP_FOOT_Y, "上滑或点空白关闭",
th->on_surface_variant, 16);
/* 状态栏画在面板体之上(0-27 由 SDK 全局状态栏维护,经 draw_statusbar→sb_sdk_refresh
* 喂数据+标脏顶条;不落面板体脏区)。仅 redraw_sb=1(打开/主题变化)才喂/标脏,交互时跳过
* 以保持脏区只限面板体(顶条像素留在 back_buf,由 SDK 已合成的状态栏保持)。 */
if (redraw_sb) draw_statusbar();
}
/* 触摸滑条带命中:origin 落在 (slugX, slugY) 的滑条行带内。返回 1=命中。 */
static int ncp_slider_band(int tx, int ty, int sy)
{
return (tx >= NCP_SLIDER_X - 14 && tx <= NCP_SLIDER_X + NCP_SLIDER_W + 14 &&
ty >= sy - 30 && ty <= sy + 24);
}
/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘)。
* 复用设置页方案:亮度 clamp [bmin,100];音量 vol_clamp 上限 + 跳步吸附 + DAC 节流去抖。
* 返回 1=值实际变更(需重绘面板/刷状态栏);0=吸附+钳制后与当前已提交值相同(懒跳过:
* 不写 DAC/背光、不刷状态栏、不标脏)→ 拖动去抖,消除逐事件刷状态栏/DAC 的卡顿(真机②修复)。
* 亮度写背光仅 sysfs 不引额外节流;音量写 DAC 节流(meow_dac_vol_write_throttled/flush 同 s_commit)。 */
static int ncp_slider_apply(int which, int tx, int save_now)
{
if (which == 1) {
int bmin = 1; meow_settings_get_int("display.brightness_min", &bmin);
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, bmin, 100);
v = mnp_brightness_clamp(v, bmin); /* 下限 brightness_min(≥1)、上限 100 */
int cur = 0; meow_settings_get_int("display.brightness", &cur);
if (!save_now && v == mnp_brightness_clamp(cur, bmin)) return 0; /* 去抖:值未变(含 clamp 后) */
meow_settings_set_int("display.brightness", v);
write_brightness(v, "notif_center bright_slider");
g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */
paint_sb_right();
if (save_now) meow_settings_save();
return 1;
} else {
int vmax = vol_clamp(settings_volume_max); /* 音量上限(尊重 audio.volume_max) */
int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, vmax);
v = mnp_step_round(v, settings_volume_step); /* 跳步吸附(默认 step=1 → 连续同设置页) */
v = vol_clamp(v); /* 0..MEOW_VOL_MAX + 上限 clamp */
if (!save_now && v == settings_volume_get()) return 0; /* 去抖:值未变跳过写 DAC/刷状态栏 */
meow_settings_set_int("audio.volume", v);
g_sys.vol = settings_volume_get(); /* 状态栏数值源=最近写入值(含 clamp 后) */
paint_sb_right();
if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } /* 抬手=无条件直写 DAC */
else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */
return 1;
}
}
/* 通知点击:dismiss(标记阅读)+ 按 route_status(FRAMEWORK_DESIGN §4)产出 notif_click_t *click。
* status 0=OK 跳转(chain 解析出链路;媒体无 target 也 status=0,chain 置空=裸启动主屏);
* status 1=NO ROUTE(源 app 在,但无 target_activity / 路由未注册);
* status 2=SOURCE NOT FOUND(源 app 不在 apps[] 或 exec 不可用)。
* 返回 1=应关闭面板(点击即关;status 1/2 由 run_menu 弹 toast,不崩)。 */
static int ncp_row_tap(int tx, int ty, int scroll, notif_click_t *click)
{
(void)tx; /* 行命中仅依赖 ty/scroll;tx 保留作接口一致(0-warning) */
int eff = ty - ncp_list_top() + scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
if (row < 0 || row >= g_nrows) return 0;
const ncp_row_t *r = &g_rows[row];
meow_notif_dismiss(r->id); /* 点击 = 阅读,撤下 */
if (click) {
click->app_idx = -1;
click->status = NOTIF_CLICK_STATUS_NOROUTE;
click->chain[0] = 0;
}
/* 源 app 定位(apps[] 内且 exec ok):找不到 → SOURCE NOT FOUND(不崩,toast) */
int src = -1;
if (g_ncp_apps)
for (int i = 0; i < g_ncp_napps; i++)
if (!strcmp(g_ncp_apps[i].id, r->app_id) && g_ncp_apps[i].ok) { src = i; break; }
if (src < 0) {
if (click) click->status = NOTIF_CLICK_STATUS_NOSRC;
return 1;
}
/* 跳转判定:M5 接 M3 —— target_activity 解析出链路则跳;媒体通知(无 target)整 app 拉起主屏;
* 其余无路由 → NO ROUTE(不崩,toast)。 */
int depth = 0;
char chain[MEOW_CHAIN_MAX] = "";
if (r->target[0] && resolve_chain(r->app_id, r->target, chain, &depth) == MEOW_OK) {
if (click) { click->app_idx = src; click->status = NOTIF_CLICK_STATUS_OK;
strncpy(click->chain, chain, MEOW_CHAIN_MAX - 1);
click->chain[MEOW_CHAIN_MAX - 1] = 0; }
} else if (!strcmp(r->category, MEOW_NOTIF_MEDIA_CAT)) {
if (click) { click->app_idx = src; click->status = NOTIF_CLICK_STATUS_OK;
click->chain[0] = 0; } /* 媒体:整 app 前台(无 activity 链则进主屏) */
} else {
if (click) click->status = NOTIF_CLICK_STATUS_NOROUTE;
}
return 1; /* 点击即关(status 0 由 run_menu 走 launch_activity/launch_app;1/2 弹 toast) */
}
/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */
void run_notif_center(struct app *apps, int napps, notif_click_t *click,
int live_open, int lox, int loy)
{
if (click) { click->app_idx = -1; click->status = NOTIF_CLICK_STATUS_NONE; click->chain[0] = 0; }
int list_scroll = 0;
unsigned char evbuf16[16];
g_ncp_apps = apps; g_ncp_napps = napps;
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER); kp_open(&kp_keys, DEV_KEYS);
struct kd_state kd_vold, kd_volu, kd_play, kd_pwr;
memset(&kd_vold, 0, sizeof kd_vold); memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play); memset(&kd_pwr, 0, sizeof kd_pwr);
int m_prev_vold = 0, m_prev_volu = 0, m_prev_play = 0, m_prev_pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) m_prev_vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) m_prev_volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) m_prev_play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) m_prev_pwr = 1;
}
int tfd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (tfd >= 0) { unsigned char ev[16]; while (read(tfd, ev, sizeof ev) == 16) {} }
unsigned long long touch_cooldown_until = now_ms() + 300;
unsigned long long last_tick = now_ms();
unsigned long long last_act = now_ms(); (void)last_act; /* 预留 idle 计时;当前未读(0-warning) */
unsigned long long last_listchk = now_ms();
int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1;
int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */
/* 通知列表拖动滚动(方向遵循 input.swipe_invert,与设置页列表同式;关闭手势区分见 §11.3) */
int scroll_drag = 0; /* 是否处于列表区拖动滚动手势 */
int scroll_base = 0; /* 进入滚动时锁定的 list_scroll 基准(连续跟手,防触发瞬间跳变) */
int scroll_y0 = 0; /* 按下原点纵向坐标 */
int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */
int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */
int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */
/* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off):
* row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手);
* row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。
* row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */
int row_swipe_row = -1;
int row_off = 0;
int row_settle = 0;
int row_off_target = 0;
unsigned long long row_anim_at = 0;
int row_paint = 0; /* 仅重绘该行带(行左滑专用) */
/* ⑨ 面板跟手下拉、半程可回:
* panel_drag = 1 面板跟手拖动中(anim_p 直接由手指位移映射,状态时间轴暂停);
* drag_seed_reveal = 进入拖动时锁定的面板「露出高度」(= ph + off,防触发瞬间跳变);
* drag_y0 = 进入拖动时锁定手指 y(此后 reveal = seed + (tly - drag_y0),连续跟手/半程可回);
* drag_paint = 仅用动画重建路径重绘一帧(复用 §9 快照 blit,不整屏/不重排控件)。 */
int panel_drag = 0;
int drag_seed_reveal = 0;
int drag_y0 = 0;
int drag_paint = 0;
int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */
int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */
int wake_suppress = 0;
unsigned long long pwr_hold_at = 0;
int pwr_menu_state = 0, pwr_woke = 0, pwr_suppress_click = 0;
int need_sb = 1; /* 打开/主题变化时重绘状态栏;其余交互跳过(脏区只限面板体) */
/* 开关平移动画状态(§9):anim_p∈[0,NCP_ANIM_FIX],anim_target 为意图(FIX=开、0=关)。
* 仅当快照可用(anim_ok)才做平滑动画;否则降级为瞬时显示/消失(closed 兜底)。 */
int anim_p = 0, anim_target = NCP_ANIM_FIX;
int anim_last_target = NCP_ANIM_FIX; /* 记录上一步 anim_target(用于检测「意图切换」重置计时) */
unsigned long long anim_last = 0;
int anim_prev_bot = NCP_TOP; /* 上一帧面板覆盖底缘(exclusive;初始=NCP_TOP=未覆盖) */
int anim_ok = 0;
/* ⑨ live_open 入口(run_menu 热区拖动中切入):手指此刻已按下且正拖动。
* - 触摸:直接以传入原点作「已按下已锁原点」(evdev 广播,本 fd 只接后续 move/release)。
* - 进度:面板从隐藏(anim_p=0)起步,target 未定(=0),由拖动路径实时映射 anim_p;
* 松手按阈值锚定 target。默认(非 live_open)仍为 trigger 式自动展开(防御路径)。 */
if (live_open) {
td = 1; td_ok = 1; tdx = lox; tdy = loy; scroll_y0 = tdy;
anim_target = 0; anim_last_target = 0; /* 待拖动/松手锚定 */
}
/* ⑥ 读勿扰面板内开关态(开面板一次;切换在下面 tap 分支写回)。 */
{
int d = 0; meow_settings_get_bool("notify.dnd", &d);
g_ncp_dnd = d;
}
/* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0
* 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(0, g_nrows * NCP_ROW_H, ncp_list_view());
ncp_snap_init();
if (g_ncp_ph > 0) {
anim_ok = 1;
ncp_snap_bg(); /* 背景(应用列表)在面板绘制前拍 */
draw_notif_center(list_scroll, 1); /* 渲染最终位置整面板 → back_buf */
ncp_snap_panel(); /* 拍面板快照 */
/* 起点 p=0:按「上一帧为全覆盖(NCP_TOP+PH)」重建 → 整段还原为背景(面板隐藏) */
anim_prev_bot = ncp_anim_rebuild(mnp_anim_off(0, NCP_ANIM_FIX, g_ncp_ph),
NCP_TOP + g_ncp_ph);
meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
meow_ui_flush();
} else {
draw_notif_center(list_scroll, 1);
meow_dirty_full(); meow_ui_flush();
}
need_sb = 0;
anim_last = now_ms();
int need_paint = 0; /* 持久:跨 tick 保持(动画进行中延迟到稳态再画) */
int media_prev = -1; /* 阶段D附录B:上次媒体卡片显示态(检测 出现/消失 → 整面板重排) */
int media_paint = 0; /* 阶段D附录B:仅重绘媒体卡片带(进度/封面变化,局部刷新) */
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) break;
/* §10.5:App 前台叠加会话期间探测 App 存活。App 死(/proc/<pid>/stat 状态位 Z=僵尸)→
* 立即结束会话(回 run_menu 统一重绘背景,不 SIGCONT 死进程、不恢复死帧);launcher 不崩。
* 注:读 /proc 状态位而非 waitpid(WNOHANG)——waitpid 一旦检测到终止即回收子进程,会抢走
* app_launcher waitpid 循环的回收权(其状态/exit 码经统一路径处理),此处只读不回收。 */
if (g_notif_ctrl_pid > 0 && proc_is_zombie(g_notif_ctrl_pid)) {
logmsg("notif-center: app pid %d died during overlay -> close now", g_notif_ctrl_pid);
g_notif_ctrl_died = 1;
break;
}
int animating = anim_ok && (anim_p != anim_target);
/* 自动息屏(同设置页语义) */
if (meow_auto_slept && !meow_screen_on) {
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int wak = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) wak = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) wak = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) wak = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) wak = 1;
}
if (!wak && screen_off_mode == 1 && tfd >= 0) {
struct pollfd spfd = { tfd, POLLIN, 0 };
if (poll(&spfd, 1, 0) > 0) {
unsigned char ev[16];
while (read(tfd, ev, sizeof ev) == 16) {
uint16_t type = *(uint16_t *)(ev + 8), code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_KEY && code == BTN_TOUCH && val) wak = 1;
}
}
}
if (wak) {
meow_panel_on(); meow_auto_slept = 0; wake_suppress = 1; last_act = now_ms();
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
touch_cooldown_until = now_ms() + 300;
td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0; list_paint = 0;
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
/* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */
anim_p = anim_target = anim_last_target = NCP_ANIM_FIX;
if (anim_ok) anim_prev_bot = NCP_TOP + g_ncp_ph;
draw_notif_center(list_scroll, 1);
if (anim_ok) ncp_snap_panel();
meow_dirty_full(); meow_ui_flush();
anim_last = now_ms();
need_sb = 0;
}
usleep(POLL_MS * 1000);
continue;
}
/* 按键:系统语义(APP §3.4.1 红线——不映射返回/确认) */
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
int vold = 0, volu = 0, play = 0, pwr = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (kp_keys.cur[w] & (1UL << (KEY_PLAYPAUSE % 32))) play = 1;
if (kp_power.cur[w] & (1UL << (KEY_POWER % 32))) pwr = 1;
}
(void)kd_tick(&kd_vold, vold); (void)kd_tick(&kd_volu, volu);
(void)kd_tick(&kd_play, play); (void)kd_tick(&kd_pwr, pwr);
vold = kd_vold.stable; volu = kd_volu.stable;
play = kd_play.stable; pwr = kd_pwr.stable;
if (wake_suppress) {
if (!pwr && !vold && !volu && !play) wake_suppress = 0;
} else {
if (vold && !m_prev_vold) { settings_volume_adj(-settings_volume_step); need_paint = 1; }
if (volu && !m_prev_volu) { settings_volume_adj(+settings_volume_step); need_paint = 1; }
if (play && !m_prev_play) {
logmsg("notif-center: play key caught (media-reserved, no exit)");
last_act = now;
}
if (pwr && !m_prev_pwr) {
pwr_hold_at = now; pwr_menu_state = 0; pwr_suppress_click = 0; last_act = now;
if (meow_auto_slept && !meow_screen_on) pwr_woke = 1;
}
if (pwr && pwr_hold_at) {
if (now - pwr_hold_at >= (unsigned long long)power_forceoff_ms && !pwr_menu_state) {
if (sim_no_exec) logmsg("notif-center pm: [sim-no-exec] hold %dms -> would forceoff", power_forceoff_ms);
else { logmsg("notif-center pm: hold %dms -> force poweroff", power_forceoff_ms); meow_power_action(POWER_SHUTDOWN); }
} else if (now - pwr_hold_at >= (unsigned long long)power_longpress_menu_ms && !pwr_menu_state) {
pwr_menu_state = 1;
logmsg("notif-center pm: hold %dms -> power menu", power_longpress_menu_ms);
show_power_menu();
last_act = now_ms();
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1;
memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu); memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL); kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
after_modal = 1; need_paint = 1;
goto ncp_keys_seeded;
}
}
if (!pwr && m_prev_pwr) {
if (pwr_suppress_click)
logmsg("notif-center pm: release after menu -> no toggle");
else if (pwr_hold_at && (now - pwr_hold_at < (unsigned long long)power_longpress_menu_ms) && !pwr_menu_state && !pwr_woke) {
logmsg("notif-center pm: single-click -> toggle panel");
meow_toggle_panel(); meow_auto_slept = 0; last_act = now_ms(); need_paint = 1;
} else if (pwr_woke) {
logmsg("notif-center pm: release after wake -> no toggle");
}
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 0;
}
}
if (vol_ramp_dir != 0) {
int held = (vol_ramp_dir > 0) ? volu : vold;
int going = vol_ramp_tick(held);
g_sys.vol = settings_volume_get();
paint_sb_right();
need_paint = 1;
if (!going) logmsg("notif-center vol ramp end -> %d", g_sys.vol);
}
m_prev_vold = vold; m_prev_volu = volu; m_prev_play = play; m_prev_pwr = pwr;
ncp_keys_seeded:
;
/* 触摸 */
if (tfd >= 0 && meow_screen_on) {
/* 面板是否就位(稳态开):就位才允许控件交互;动画中抑制 chip/滑条/通知行命中
* (面板在滑,控件坐标已定但只部分可见,命中会误触——见 §9.5)。 */
int open_settled = anim_ok ? (anim_target >= NCP_ANIM_FIX && anim_p >= NCP_ANIM_FIX) : 1;
struct pollfd pfd = { tfd, POLLIN, 0 };
if (poll(&pfd, 1, 0) > 0) {
while (read(tfd, evbuf16, sizeof evbuf16) == 16) {
uint16_t type = *(uint16_t *)(evbuf16 + 8), code = *(uint16_t *)(evbuf16 + 10);
int32_t val = *(int32_t *)(evbuf16 + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) tlx = val;
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) tly = val;
if (td && !td_ok && tlx >= 0 && tly >= 0) {
tdx = tlx; tdy = tly; td_ok = 1;
scroll_y0 = tdy; /* 列表拖动基准:按下原点(进滚动时才锁 scroll_base) */
/* 按下原点落在滑条带 → 开始拖动(仅面板就位) */
if (open_settled && drag_slider == 0) {
if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
}
}
if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; }
else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; }
/* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop,
* 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。
* 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含
* row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大:
* 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
int ddx = tlx - tdx, ddy = tly - tdy;
int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy;
if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) {
int eff = tdy - ncp_list_top() + list_scroll;
int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H);
/* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */
if (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
row >= 0 && row < g_nrows) {
row_swipe_row = row;
row_off = mnp_swipe_clamp(ddx, NCP_ROW_W);
row_settle = 0; row_off_target = 0;
row_anim_at = now_ms();
g_ncp_swipe_row = row; g_ncp_swipe_off = row_off;
row_paint = 1;
}
}
}
/* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */
if (row_swipe_row >= 0 && td_ok && !row_settle) {
row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
row_paint = 1;
}
/* ⑨ 面板跟手拖动(进入):竖向位移达 slop、起点不在列表溢出滚动区(归 §11)、
* 未锁行左滑/滚动/滑条 → 进入拖动。锁定当前面板露出高度 + 当前手指 y,
* 此后 reveal = seed + (tly - drag_y0),方向无关、连续可反向(半程可回)。 */
if (td_ok && !drag_slider && !scroll_drag && row_swipe_row < 0 && !panel_drag) {
int ady2 = (tly - tdy) < 0 ? (tdy - tly) : (tly - tdy);
if (ady2 >= S_SCROLL_SLOP) {
int av = g_nrows * NCP_ROW_H;
int vw = ncp_list_view();
int in_over = (tdy >= ncp_list_top() && tdy < NCP_LIST_BOT &&
mnp_scroll_max(av, vw) > 0);
if (!in_over) {
int cur_off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph);
drag_seed_reveal = (g_ncp_ph > 0) ? (g_ncp_ph + cur_off) : 0;
drag_y0 = tly;
panel_drag = 1;
}
}
}
/* ⑨ 跟手:panel_drag 时 reveal=seed+位移 → progress 更新 anim_p;拖动期时间轴暂停。 */
if (panel_drag && td_ok) {
int reveal = mnp_drag_reveal(drag_seed_reveal, tly - drag_y0, g_ncp_ph);
int np = mnp_drag_progress(reveal, NCP_ANIM_FIX, g_ncp_ph);
if (np != anim_p) { anim_p = np; drag_paint = 1; }
}
/* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 +
* 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式);
* 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */
if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) {
int ddy = tly - scroll_y0;
if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) {
int total = g_nrows * NCP_ROW_H;
int view = ncp_list_view();
if (mnp_scroll_max(total, view) > 0 &&
tdy >= ncp_list_top() && tdy < NCP_LIST_BOT) {
scroll_drag = 1;
meow_settings_get_bool("input.swipe_invert", &scroll_invert);
scroll_base = mnp_scroll_base(list_scroll, ddy, scroll_invert);
scroll_rebound = 0;
}
}
}
if (scroll_drag && td_ok) {
int total = g_nrows * NCP_ROW_H;
int view = ncp_list_view();
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
list_scroll = mnp_scroll_clamp(target, total, view);
scroll_rebound = mnp_scroll_rebound(target, mnp_scroll_max(total, view),
NCP_SCROLL_REBOUND_TOL);
list_paint = 1;
}
} else if (type == EV_KEY && code == BTN_TOUCH) {
if (val && !td) {
if (now_ms() < touch_cooldown_until) { tlx = tly = -1; }
else {
td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0;
scroll_drag = 0; scroll_base = 0; scroll_y0 = 0;
scroll_invert = 0; scroll_rebound = 0;
/* ⑦ 新触摸:清行左滑(防上一手势残留) */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
/* ⑨ 新触摸:清面板跟手(防上一拖动残留) */
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
}
} else if (!val && td) {
td = 0;
if (drag_slider) { /* 滑条拖动结束 → 落盘 */
ncp_slider_apply(drag_slider, tlx >= 0 ? tlx : tdx, 1);
drag_slider = 0; need_paint = 1;
} else if (td_ok) {
int dx = tlx - tdx, dy = tly - tdy;
int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy;
/* 左缘左滑 = 关闭(复用全局左缘返回语义;动画中同样可打断)——
* 优先于列表滚动落定,保「左缘返回关」语义不受滚动影响 */
if (meow_edge_back(tdx, dx, dy)) {
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
if (row_swipe_row >= 0) {
/* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。
* 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */
/* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */
row_off = mnp_swipe_clamp(dx, NCP_ROW_W);
g_ncp_swipe_off = row_off;
if (mnp_swipe_delete(row_off, NCP_ROW_W,
NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) {
row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */
} else {
row_off_target = 0; /* 回弹复原 */
}
row_settle = 1; row_anim_at = now_ms();
} else if (scroll_drag) {
/* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值);
* 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。
* 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过
* 下方 mnp_vertical_up 关闭判定。 */
int total = g_nrows * NCP_ROW_H;
int view = ncp_list_view();
int target = mnp_scroll_target(scroll_base, tly - scroll_y0, scroll_invert);
list_scroll = mnp_scroll_clamp(target, total, view);
if (scroll_rebound) {
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
list_paint = 1;
scroll_base = 0; scroll_y0 = 0; scroll_rebound = 0; scroll_invert = 0;
} else if (panel_drag) {
/* ⑨ 面板跟手拖动松手:按阈值锚定 target(≥40% → 补到展开;
* <40% 或反向拖回 → 回 0 收回 = 半程取消/关闭过阈值)。时间轴接管
* 把 anim_p 补到位;不误关面板、不走 tap/上滑/点空白(本手势锁定)。 */
int go_open = mnp_drag_anchor(anim_p, NCP_ANIM_FIX, NCP_DRAG_THR_PCT);
if (anim_ok) anim_target = go_open ? NCP_ANIM_FIX : 0;
else if (!go_open) closed = 1; /* 无快照降级:收回 → 直接退出 */
panel_drag = 0; drag_paint = 0; drag_seed_reveal = 0; drag_y0 = 0;
} else if (adx < 60 && ady < 60) { /* tap */
if (animating) break; /* 动画进行中:tap 忽略(控件命中会误触,见 §9.5) */
int tx = tdx, ty = tdy;
int hit = 0;
for (int c = 0; c < NCP_CHIP_COUNT; c++)
if (ncp_chip_hit(c, tx, ty)) {
int ar = ncp_chip_apply(c);
if (ar == 1) need_sb = 1; /* 主题变化 → 重刷状态栏 */
else if (ar == 2) {
after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */
/* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配,
* 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */
memset(&kd_pwr, 0, sizeof kd_pwr);
memset(&kd_vold, 0, sizeof kd_vold);
memset(&kd_volu, 0, sizeof kd_volu);
memset(&kd_play, 0, sizeof kd_play);
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN);
m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
/* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空,
* 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */
if (tfd >= 0) {
unsigned char ev2[16];
while (read(tfd, ev2, sizeof ev2) == 16) {}
}
}
hit = 1; need_paint = 1; break;
}
if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { hit = 1; if (ncp_slider_apply(1, tx, 1)) need_paint = 1; }
else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { hit = 1; if (ncp_slider_apply(2, tx, 1)) need_paint = 1; }
/* ⑥ 勿扰面板内开关(列表上方小 toggle):tap 即切换(整带命中) */
if (!hit && ncp_dnd_hit(tx, ty)) {
int dv = 0; meow_settings_get_bool("notify.dnd", &dv);
meow_settings_set_bool("notify.dnd", dv ? 0 : 1);
meow_settings_save();
g_ncp_dnd = dv ? 0 : 1;
ncp_refresh_rows(); /* 按 dnd 过滤重采集 */
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
hit = 1; need_paint = 1;
}
/* ⑧ 一键清除(勿扰对称处右半段):先转储留档 → 再清空显示(留 ongoing/media/progress)。 */
if (!hit && ncp_clear_hit(tx, ty)) {
int n = ncp_clear_all();
logmsg("notif-center: clear %d notif(s) (dumped first, kept ongo/media/progress)", n);
/* 清除后取消行左滑(下标会变),再重采集 + 重钳 + 整面板重画。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
hit = 1; need_paint = 1;
}
/* 阶段D附录B:顶部媒体卡片按钮/进度(含 seek)→ 下发 PLAYCTL。
* 卡片命中固定于列表上方,先于列表行命中处理。 */
if (!hit && meow_media_has_source() && ncp_media_card_tap(tx, ty)) {
hit = 1; need_paint = 1;
} else if (!hit && ty >= ncp_list_top() && ty < NCP_LIST_BOT) {
if (ncp_row_tap(tx, ty, list_scroll, click)) {
hit = 1;
if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */
else closed = 1;
break;
}
}
if (!hit) { /* 点空白(标题/把手/提示/分隔线/列表外)→ 关闭 */
if (anim_ok) anim_target = 0; else closed = 1;
break;
}
} else if (mnp_vertical_up(dx, dy, MENU_SWIPE_THRESH)) {
if (anim_ok) anim_target = 0; else closed = 1; /* 面板内上滑 → 关闭 */
break;
} else if (anim_ok && anim_target == 0 &&
mnp_top_swipe_down(tdx, tdy, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
anim_target = NCP_ANIM_FIX; /* 收回途中顶部热区下滑 → 重新打开(平滑转向) */
}
/* 其余横滑/下滑忽略(不关闭不动作) */
}
td_ok = 0;
}
}
}
}
}
if (closed) break; /* 非动画兜底:上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */
/* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */
if (now - last_listchk >= 500) {
last_listchk = now;
/* 阶段D附录B:面板驻留时排空 IPC(App 上报 META 更新媒体会话)+ 媒体会话 TTL,再做展示刷新 */
meow_ipc_process();
meow_notif_tick();
meow_media_tick();
int m_on = meow_media_has_source() ? 1 : 0;
if (m_on != media_prev) {
media_prev = m_on; /* 卡片出现/消失 → 列表顶缘变化,需整面板重排 */
need_paint = 1;
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, ncp_list_view());
} else if (m_on) {
media_paint = 1; /* 已有源:进度/封面随 META 变化 → 仅重绘卡片带 */
}
if (ncp_rows_changed()) {
/* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。⑨ 同时取消面板跟手。 */
row_swipe_row = -1; row_off = 0; row_settle = 0;
row_off_target = 0; row_anim_at = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
panel_drag = 0; drag_seed_reveal = 0; drag_y0 = 0; drag_paint = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
need_paint = 1;
}
}
if (media_paint) { ncp_repaint_media_card(); media_paint = 0; }
/* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。
* 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */
if (anim_ok && anim_p != anim_target && !panel_drag) {
unsigned long long anow = now_ms();
int dt;
if (anim_target != anim_last_target) {
/* 意图刚切换(打开↔关闭):重置计时起点,本 tick 不计时,
* 避免稳态闲置后 anim_last 陈旧导致首帧就跳到目标(不产生动画)。 */
anim_last_target = anim_target;
anim_last = anow;
dt = 0;
} else {
dt = (int)((anow > anim_last) ? (anow - anim_last) : 0);
anim_last = anow;
}
int np = mnp_anim_step(anim_p, anim_target, NCP_ANIM_FIX,
(unsigned long long)dt, NCP_ANIM_MS);
if (np != anim_p) {
anim_p = np;
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); /* §9 快照 blit 平移(只刷面板矩形) */
}
}
if (anim_ok && !panel_drag && !td && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 且无按下触摸 → 真正退出子循环 */
/* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。
* 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。
* 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */
if (row_swipe_row >= 0 && row_settle) {
unsigned long long rnow = now_ms();
int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0);
row_anim_at = rnow;
int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE);
if (noff != row_off) {
row_off = noff;
g_ncp_swipe_off = noff;
row_paint = 1;
}
if (noff == row_off_target) {
if (row_off_target == 0) {
/* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */
ncp_repaint_row(list_scroll, row_swipe_row, 0);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */
} else {
/* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */
if (row_swipe_row >= 0 && row_swipe_row < g_nrows)
meow_notif_dismiss(g_rows[row_swipe_row].id);
row_swipe_row = -1; row_settle = 0; row_off = 0;
row_off_target = 0; row_paint = 0;
g_ncp_swipe_row = -1; g_ncp_swipe_off = 0;
ncp_refresh_rows();
list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H,
ncp_list_view());
need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */
}
}
}
/* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。
* need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍
* 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。
* 动画进行中:不画稳态面板(会覆盖正在滑动的帧),need_paint 保持到到位后再补画。 */
if (need_paint) {
last_act = now;
if (!(anim_ok && anim_p != anim_target)) {
draw_notif_center(list_scroll, need_sb);
need_sb = 0;
if (anim_ok) ncp_snap_panel(); /* 面板内容已变:刷新快照(供滑出动画用) */
if (after_modal) { meow_dirty_full(); after_modal = 0; }
else meow_dirty_add(0, NCP_TOP, fb_xres, fb_yres - NCP_TOP);
meow_ui_flush();
need_paint = 0;
list_paint = 0; /* 整面板已含列表:滚动重绘标记随之清除 */
}
/* 否则动画进行中:保留 need_paint,到位后(下一稳态 tick)补画最终面板 */
}
/* 仅列表区重绘(滚动专用:只刷列表带,不整面板/不刷状态栏;同受动画门控,
* 动画进行中不画稳态,list_paint 保持到到位后再补画一次列表终值)。 */
if (list_paint) {
last_act = now;
if (!(anim_ok && anim_p != anim_target)) {
ncp_repaint_list(list_scroll);
if (anim_ok) ncp_snap_panel(); /* 列表已变:刷新面板快照(供滑出动画用) */
list_paint = 0;
}
}
/* ⑨ 面板跟手拖动重绘:只刷面板矩形 + 新露出条(§9 快照 blit 平移;不整屏/不重排控件)。
* 拖动期 anim_p 直接由手指映射,故不受下方「animating」门控(非时间轴推进),直接呈现。 */
if (drag_paint) {
last_act = now;
ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok);
drag_paint = 0;
}
/* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。
* row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */
if (row_paint) {
last_act = now;
if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target))
ncp_repaint_row(list_scroll, row_swipe_row, row_off);
row_paint = 0;
}
/* ~1s 状态栏时钟 */
if (now - last_tick >= 1000) {
last_tick = now;
draw_clock();
fb_norm_buf0();
meow_ui_flush();
}
usleep(POLL_MS * 1000);
}
if (tfd >= 0) close(tfd);
kp_close(&kp_power); kp_close(&kp_keys);
ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */
ncp_rows_free();
logmsg("notification center closed");
}