/* 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"); }