Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/notif_center.c
T
MeowOS 23727cf400 feat(system): 系统框架一期 — 专项服务(性能/电源)+IPC+电源CLI+电菜单改经服务(agent-155)
- meow_svc_power: 单一二进制(svcd空参IPC常驻 /usr/data/meow_power.sock + root CLI免鉴权), 12命令(shutdown/reboot强制=0/upgrade/set-default-hiby/reboot--hiby + set-default-meow/status/screen/dac(status仅)/battery/reboot-reason/upgrade-status); 动作同源sys_do_reboot/set_boot_default_hiby/mtd5=ota:kernel2; dac on/off无控制点不支持
- meow_svc_perf: IPC(/usr/data/meow_perf.sock MSG_PERF_*) + CLI, 聚合/proc/df/thermal/version/uname JSON(与MEOWPANEL_PROTOCOL对齐)
- power_menu.c 改经 meow_power_action(launcher先转储再IPC), 删sys_do_reboot/set_boot_default_hiby; 4动作+8s强关+confirm_reboot_player+3处长按强关全经服务; launcher内 sys_do_reboot/bootmode.sh/set_boot_default_hiby 0调用
- meow_ipc.h 仅加 MSG_PERF_QUERY/RESULT(0x50/51)+MSG_POWER_ACTION/STATUS(0x60/61)+POWER_*(12)+m_perf/power_t; 未动既有
- S89meow_svcd 预置 meow_svc_perf/power.conf(trigger=boot); 各独立socket
- IPC鉴权一期=SO_PEERCRED uid==0基础(完整os.meow.*二期); 通知转储=launcher先转储再IPC(二期补launcher→服务通道); 失败安全(服务不可达→按取消,不静默不直连)
- meow_svc_power=32d6be92/meow_svc_perf=7b0f3b64/launcher=d90f09e9/svcd=fb2d608b(未动)/meow_ui.a=92e61fb8(未改); 全0warning; 备份power_menu.c.pre-svc.bak回退
2026-08-30 23:16:12 +08:00

1364 lines
74 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;
/* ⑥ 勿扰面板内开关:当前 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;
/* 短串拷贝(同 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->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);
g_sys.brightness = 100;
} else {
meow_settings_set_int("display.brightness", vmin);
write_brightness(vmin);
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();
if (g_nrows == 0) {
ui_draw(NCP_ROW_X + 8, NCP_LIST_TOP + 40, "暂无通知", th->on_surface_variant, 22);
return;
}
int total = g_nrows * NCP_ROW_H;
scroll = mnp_scroll_clamp(scroll, total, NCP_LIST_BOT - NCP_LIST_TOP);
for (int i = 0; i < g_nrows; i++) {
int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < NCP_LIST_TOP || 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 > NCP_LIST_BOT - NCP_LIST_TOP) {
int view = NCP_LIST_BOT - NCP_LIST_TOP;
int thumb_h = view * view / total; if (thumb_h < 24) thumb_h = 24;
int ty = NCP_LIST_TOP + (view - thumb_h) * scroll / (total - view);
meow_gfx_fill_round(fb_xres - 8, NCP_LIST_TOP, 5, view, 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();
fill_rect(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP, th->surface_variant);
ncp_draw_list(scroll);
meow_dirty_add(0, NCP_LIST_TOP, fb_xres, NCP_LIST_BOT - NCP_LIST_TOP);
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 ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll;
if (ry + NCP_ROW_H < NCP_LIST_TOP || 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 重绘,交互时跳过,
* 使脏区仅限面板体,绝不整屏重绘。 */
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);
/* 通知列表 */
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);
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(标记阅读)+ 若 app 有 entry 记录则请求启动。返回 1=应关闭面板。 */
static int ncp_row_tap(int tx, int ty, int scroll, int *want_launch)
{
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); /* 点击 = 阅读,撤下 */
/* 跳转:app entry 记录(apps[] 内且 ok)→ 记 want_launch(run_menu 走标准启动)。
* target_activity 为后续 M3 导航预留下的跳转目标(activity 级路由后置)。 */
if (want_launch) *want_launch = -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) {
if (want_launch) *want_launch = i;
break;
}
return 1; /* 点击即关(启动由 run_menu 执行;无记录则仅关闭) */
}
/* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */
void run_notif_center(struct app *apps, int napps, int *want_launch,
int live_open, int lox, int loy)
{
if (want_launch) *want_launch = -1;
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();
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_BOT - NCP_LIST_TOP);
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 保持(动画进行中延迟到稳态再画) */
for (;;) {
unsigned long long now = now_ms();
if (sigterm_flag) 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 >= 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 >= 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 < 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_BOT - NCP_LIST_TOP;
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_BOT - NCP_LIST_TOP;
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_BOT - NCP_LIST_TOP;
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_BOT - NCP_LIST_TOP;
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_BOT - NCP_LIST_TOP);
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_BOT - NCP_LIST_TOP);
hit = 1; need_paint = 1;
}
if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) {
if (ncp_row_tap(tx, ty, list_scroll, want_launch)) {
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;
meow_notif_tick();
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_BOT - NCP_LIST_TOP);
need_paint = 1;
}
}
/* 开关动画推进(§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_BOT - NCP_LIST_TOP);
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");
}