diff --git a/FRAMEWORK_DESIGN.md b/FRAMEWORK_DESIGN.md index 1ec4cf5..8cd0da8 100644 --- a/FRAMEWORK_DESIGN.md +++ b/FRAMEWORK_DESIGN.md @@ -47,6 +47,8 @@ struct ipc_hdr { /* LE */ | 0x0021 | MSG_NOTIFY_ACK | ↓ | 落库结果 | | 0x0022 | MSG_NOTIFY_DISMISS | ↑ | 撤下通知 | | 0x0023 | MSG_NOTIFY_LIST | ↓ | 下发通知清单(重同步) | +| 0x0025 | MSG_NOTIF_OPEN | ↑ | **(拟,号待 `meow_ipc.h` 回填)** App 检测到顶部下滑→请求 launcher 展开通知中心叠加会话(App 前台,见 §10.5) | +| 0x0026 | MSG_NOTIF_CLOSE | ↑ | **(拟,号待 `meow_ipc.h` 回填,可选用途)** App 告知结束/退出→launcher 收尾兜底(见 §10.5) | | 0x0030 | MSG_ACT_DECLARE | ↑ | (重)声明路由表 | | 0x0031 | MSG_ACT_QUERY | ↑ | 解析 activity 链路 | | 0x0032 | MSG_ACT_RESULT | ↓ | 返回链路 | @@ -166,6 +168,37 @@ meow_sysui(系统进程总监,meow_svcd 托管;最小依赖/不渲染/最 - **launcher 主界面 / 设置页 / 电源菜单全部迁到 SDK 控件**:逻辑/渲染走 SDK(MEOW_APP_SDK §6.1 ② App 跨 Activity 控件、§5.4 A① 分组设置表单、§5.7 B④ 面板等模板),MD3 化、日/夜主题(`display.theme_mode`)、全局 UI 统一。 - **不让 app 自建叠加**:app 一律经 SDK 标准能力;系统级叠加(状态栏/通知中心/电源菜单/全局手势)只在系统进程内实现。迁移后 launcher 与 APP 用同一套 SDK 渲染地基,观感/生命周期统一。 +### 10.5 App 前台划出通知中心(跨进程叠加会话)[源:2026-08-30] + +> 定位:补齐「全屏 App(如 hello)前台时,App 进程内顶部下滑也能唤起**由 launcher 渲染**的通知中心」。 +> 详细实现方案见 `devdocs/plan/NOTIF_CENTER_APP_OVERLAY_PLAN.md`;本节为**定案决策**(归属/归属不变/协议/会话/故障隔离)。 + +- **交互与归属(不变)**:App 前台时顶部下滑,**App 进程只负责「检测手势 + 经 IPC 上报」**;通知中心 + **仍由 launcher 渲染**(符合 §10.2「系统 UI 归 launcher」/`MEOW_APP_SDK §6.1 ③`/`NOTIFICATION_SYSTEM §9`); + **App 进程不渲染系统 UI**(不叠加状态栏/通知中心)。手势检测在 App 进程 SDK(`meow_ui_input.c` 已把 + 「起点在顶部热区+向下主导」识别为系统保留手势 `MEOW_EV_NOTIF_PULL` → 路由 `meow_ui_sys_notif_pull()`)。 +- **跨进程握手(复用现有 IPC,不新增 socket)**:复用 `AF_UNIX /usr/data/meow_ipc.sock`(`meow_ipc.h + MEOW_IPC_PATH`,launcher `ipc_thread` bind/listen)。拟新增上行消息: + - `MSG_NOTIF_OPEN`(App→launcher):App 检测到顶部下滑→请求展开通知中心叠加会话;负载 `m_notif_open_t` + (app_id/event_seq/origin_x/origin_y/hotzone/flags)。launcher 回 `MSG_ACK` 受理(best-effort,App 不强依赖)。 + - `MSG_NOTIF_CLOSE`(App→launcher,可选用途):App 告知结束/退出→launcher 收尾(清 active/SIGCONT 兜底)。 + - 号段/结构以 `meow_ipc.h` 回填为准(拟 `0x0025/0x0026`,见 §13 决策点)。 +- **launcher 叠加会话(复用电源菜单对 App 的冻结模板)**:收到 `MSG_NOTIF_OPEN`(且当前在 App 前台会话, + `launch_app` waitpid 循环内)→ **SIGSTOP App** → 读回 App 帧(`memcpy` fb0 buffer0 → back_buf,App 帧恒 + buffer0 不走 `fb_backup_player`;同 `pm_capture_app_frame`)→ 写 `/tmp/meow_notif_active`(同 + `/tmp/meow_power_menu_active`,让 watchdog 让位)→ 叠加渲染 `draw_notif_center` + `meow_ui_flush` → + **接管输入**(`open(DEV_TOUCH)`+`kp_open`,App 已冻不影响)→ 关闭(上滑/点空白/左缘左滑/点通知/物理键)→ + 恢复 App 帧 → `unlink(active)` → **SIGCONT App** → 重播输入基线。差异:背景=App 帧(非应用列表); + 关闭后回 App(非回 run_menu)。 +- **App 崩溃/卡死**:launcher 会话子循环周期用 `waitpid(pid,&st,WNOHANG)`/`kill(pid,0)` 探测;App 死→立即结束会话 + (不 SIGCONT 死进程、不恢复死帧),走既有**统一应用退出路径**归位;launcher 不崩(`kill(pid,0)` 存活检测 + 与 watchdog 让位语义同 `meow_launcherd.c` ~8533-8555)。 +- **全屏/沉浸不冲突**:全屏/收合时 SDK 顶部热区缩小为 `MEOW_SBAR_HOTZONE_FS=4`(或提示线高),**仍保留顶部 + 一处下拉入口**(`meow_ui_statusbar_hotzone()`),spec-ref §3.1/A-13/§5.1 已定「全屏下顶部下滑仍开通知中心」。 +- **与 M3 关系**:本方案是 M3「前台会话」的一个切面(App 前台会话+跨进程叠加)。**本方案先行**(握手+叠加会话+ + 故障隔离,不依赖 M3);通知点击 `target_activity` 的 activity 级跳转 / 点击通知**跨 app 启动** / nav_stack 回退 + **留 M3**(首期:点通知 = dismiss + 关面板 + SIGCONT 回当前 App)。 + --- ## 附录 A(用户追加):后台运行 / 后台播放权限 diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md index 2b28101..a0d52eb 100644 --- a/MEOW_APP_SDK.md +++ b/MEOW_APP_SDK.md @@ -552,6 +552,10 @@ void meow_ui_global_remove(int id); - **SDK 提供系统组件 API 仅供系统进程链接调用**:SDK 保留系统级控件内部接口位(上面 ③);**用户 App 拿不到**、**不可自建悬浮层/全局叠加**——App 这类需求只能走 SDK 标准能力。 - **进程隔离(核心)**:SDK 负责**系统组件渲染**的进程(launcher)与负责 **APP 渲染**的进程**相互独立**——APP 卡死/崩溃时,系统 UI(launcher)**不崩**,始终可息屏/返回/下拉通知中心(对应 APPS §3.4.1 物理键全局接管、APP 禁映射)。 - **APP 侧职责**:只画自己的内容;经 IPC 上报**全屏声明**(`fullscreen`/`route..fullscreen`)、**内容区域**(inset)与**前台 activity**;系统 UI 据此做叠画 + 命中分流(§6.1 交互分层:③ 系统全局 → ② App 全局 → ① 当前 Activity 页面控件)。 +- **App 前台划出通知中心(跨进程,[源:2026-08-30])**:全屏 App 前台时顶部下滑——**App 进程 SDK 只检测手势 + (`MEOW_EV_NOTIF_PULL`→`meow_ui_sys_notif_pull()`)并经 IPC 上报**(`MSG_NOTIF_OPEN`),**不渲染系统 UI**; + 通知中心由 launcher 渲染(SIGSTOP App+读回其帧+叠加 `run_notif_center`+接管输入+关闭恢复), + 见 `FRAMEWORK_DESIGN §10.5` / `devdocs/plan/NOTIF_CENTER_APP_OVERLAY_PLAN.md`。 --- diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md index 1080b84..f7b291b 100644 --- a/NOTIFICATION_SYSTEM.md +++ b/NOTIFICATION_SYSTEM.md @@ -73,6 +73,10 @@ target_activity(可选, 来自 hints) / progress(可选 int) / urgency(0-3, 缺 - **交互**:**状态栏下滑展开**(用户已定);**上滑 / 点面板外关**;**左缘返回关**(与全局左缘返回/顶部滑条按起始位置分流,见 spec-ref §5.1)。列表显示通知(app 图标 + summary + body + target Activity),点击按路由表解析跳转(M3 导航栈),下拉关闭/清除。 - **协议/约束**:遵守 freedesktop 字段(summary/body/app_icon/actions/hints/expire_timeout/replaces_id)、配额(每 App 4 类型×每类型 4、媒体单例)、TTL、转储(10s + `MeowOS/notifications/` 10MB ring)、`urgency`/`icon`/`progress`(见 §5-§8)。 - **依赖**:M2 存储 + M3 导航栈(点击跳转)+ SDK 控件基座。**阶段 B 进行中。**(叠加/与播放器帧备份的互斥边界见 DEV_NOTES #16。) +- **App 前台变体(跨进程,[源:2026-08-30])**:全屏 App 前台时,App 进程内顶部下滑也能唤起通知中心——**App + 进程只检测手势并经 IPC 上报**(`MSG_NOTIF_OPEN`,见 `FRAMEWORK_DESIGN §10.5`),**通知中心仍由 launcher + 渲染**(SIGSTOP App+读回其帧+叠加 `run_notif_center`+接管输入+关闭恢复),符合本节「渲染主体=系统进程(launcher)」。 + 仍遵守 freedesktop 字段/配额/TTL/urgency/icon/progress(§5-§8),数据在 launcher `meow_notif` 直读,不跨进程传全量。 ## 10. 分步实施建议 1. **M2a 通知存储 + 原生 IPC**:按上面的配额(category 级)+ TTL 10s 转储钩子 + 系统事件源(系统作 App)。→ 可先落地。 diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 5be96a2..ee8bfbe 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/meow_launcherd.c b/patch_v2/meow_launcherd/meow_launcherd.c index d9c0ead..db40965 100644 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ b/patch_v2/meow_launcherd/meow_launcherd.c @@ -6779,7 +6779,7 @@ done: #define NCP_CHIP_H 44 #define NCP_CHIP_W 70 #define NCP_CHIP_GAP 8 -#define NCP_CHIP_N 5 +#define NCP_CHIP_N 4 #define NCP_SLIDER_X 20 #define NCP_SLIDER_W (fb_xres - 40) #define NCP_SL1_Y 162 /* 亮度滑条 track 顶(label 在 -26 =136) */ @@ -6795,10 +6795,16 @@ done: #define NCP_TEXT_W (NCP_ROW_W - (NCP_TEXT_X - NCP_ROW_X) - 10) #define NCP_FOOT_Y 700 #define NCP_MAX_ROWS 24 /* 面板一次缓冲的通知行上限(防御) */ +#define NCP_SCROLL_REBOUND_TOL 48 /* px:列表拖到顶/底仍继续拖过此容差 → 判回弹(关闭语义), + 避免在边界小幅抖动即误关(> S_SCROLL_SLOP=40) */ + +/* ---- 开关平移动画(§9:状态时间轴 + 快照 blit 平移,不整屏重绘)---- */ +#define NCP_ANIM_FIX 1024 /* 进度 p 的定点分母(1/1024 精度) */ +#define NCP_ANIM_MS 180 /* 单程打开/关闭时长(ms,真机可调) */ /* chip 索引 */ enum { NCP_CHIP_WIFI=0, NCP_CHIP_BT=1, NCP_CHIP_THEME=2, NCP_CHIP_BRIGHT=3, - NCP_CHIP_VOL=4, NCP_CHIP_COUNT=5 }; + NCP_CHIP_COUNT=4 }; /* 通知行快照(面板读取字段 + 缓存的 icon 句柄)。 */ typedef struct { @@ -6892,6 +6898,86 @@ static int ncp_rows_changed(void) return c != 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; +} + /* 把文本按宽度截断(UTF-8 安全:不切开多字节码),超出加 "..."。 */ static void ncp_text(int x, int y, const char *s, int size, uint32_t color, int maxw) { @@ -6937,7 +7023,7 @@ static void ncp_sb_icon(int icon, int slot_x, int cy, uint32_t col, int ink_h) g_sb_icon_cy = saved; } -/* chip 位置(5 个,居中排布)。返回下标对应 chip 的 x 左缘。 */ +/* 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; @@ -6950,7 +7036,7 @@ static int ncp_chip_hit(int chip, int tx, int ty) return (tx >= x && tx < x + NCP_CHIP_W && ty >= NCP_CHIP_Y && ty < NCP_CHIP_Y + NCP_CHIP_H); } -/* 面板当前 5 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */ +/* 面板当前 4 个 chip 的状态(① 能否写入/取值在 meow_settings_*;② 显示用)。 */ static void ncp_chip_state(int chip, int *on, char *val, int vlen) { *on = 0; @@ -6971,10 +7057,6 @@ static void ncp_chip_state(int chip, int *on, char *val, int vlen) int vc = mnp_brightness_clamp(v, vmin); if (val && vlen > 0) snprintf(val, vlen, "%d%%", vc); *on = (vc >= 50); /* 过半视为"亮"态 */ - } else if (chip == NCP_CHIP_VOL) { - int v = 0; meow_settings_get_int("audio.volume", &v); - if (val && vlen > 0) snprintf(val, vlen, "%d%%", v); - *on = (v > 0); /* 非静音=开 */ } } @@ -7016,15 +7098,6 @@ static int ncp_chip_apply(int chip) g_sys.brightness = vmin; } paint_sb_right(); - } else if (chip == NCP_CHIP_VOL) { - /* 快捷音量:静音<->恢复上一次非零音量。 */ - static int last_nz = 60; - int cur = 0; meow_settings_get_int("audio.volume", &cur); - if (cur > 0) { last_nz = vol_clamp(cur); meow_settings_set_int("audio.volume", 0); } - else { meow_settings_set_int("audio.volume", vol_clamp(last_nz)); } - g_sys.vol = settings_volume_get(); - paint_sb_right(); - meow_dac_vol_flush(); /* 抬手=无条件直写真实 DAC */ } meow_settings_save(); return 0; @@ -7085,6 +7158,18 @@ static void ncp_draw_list(int scroll) } } +/* 仅重绘列表可见区(滚动专用;不整面板、不刷状态栏)。先清列表底(面板 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(); +} + /* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。 * 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过, * 使脏区仅限面板体,绝不整屏重绘。 */ @@ -7092,8 +7177,11 @@ 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; - fill_rect(0, 0, W, H, th->surface); - /* 面板体(状态栏之下 surface_variant) */ + /* 状态栏带 [0,NCP_TOP) 仅当 redraw_sb=1(打开/主题变化)时清底,随后由下方 draw_statusbar() + * 覆盖重绘;redraw_sb=0(交互重绘,如拖滑条)时**绝不触碰**——保留 back_buf 中 paint_sb_right() + * 已绘好的状态栏,避免被清成 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); /* 把手 */ @@ -7104,7 +7192,7 @@ static void draw_notif_center(int scroll, int redraw_sb) ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16); /* 快捷开关行 */ - static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "音量" }; + 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++) { @@ -7122,10 +7210,9 @@ static void draw_notif_center(int scroll, int redraw_sb) 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_VOL) ncp_sb_icon(SB_I_VOL, icon_cx, icon_cy, fg, 22); char label[24]; snprintf(label, sizeof label, "%s", chip_lbl[c]); - if (val[0] && (c == NCP_CHIP_BRIGHT || c == NCP_CHIP_VOL)) + 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); @@ -7172,25 +7259,37 @@ static int ncp_slider_band(int tx, int ty, int sy) ty >= sy - 30 && ty <= sy + 24); } -/* 滑条值应用(拖动实时 set_*,save_now=抬手落盘;亮度 clamp、音量 clamp)。 */ -static void ncp_slider_apply(int which, int tx, int save_now) +/* 滑条值应用(拖动实时 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; + g_sys.brightness = v; /* 状态栏数值源=最近写入值(含 clamp 后) */ paint_sb_right(); if (save_now) meow_settings_save(); - } else if (which == 2) { - int v = mnp_slider_val_from_x(tx, NCP_SLIDER_X, NCP_SLIDER_W, 0, - vol_clamp(settings_volume_max)); + 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(); + g_sys.vol = settings_volume_get(); /* 状态栏数值源=最近写入值(含 clamp 后) */ paint_sb_right(); - if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } - else meow_dac_vol_write_throttled(); + if (save_now) { meow_dac_vol_flush(); meow_settings_save(); } /* 抬手=无条件直写 DAC */ + else meow_dac_vol_write_throttled();/* 拖动中=墙钟节流写 DAC */ + return 1; } } @@ -7243,7 +7342,14 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) int td = 0, td_ok = 0, tdx = -1, tdy = -1, tlx = -1, tly = -1; int drag_slider = 0; /* 0=无, 1=亮度, 2=音量 */ - int closed = 0; /* 本次触发放置:关面板(跳回 run_menu) */ + /* 通知列表拖动滚动(方向遵循 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; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */ + int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */ int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */ int wake_suppress = 0; unsigned long long pwr_hold_at = 0; @@ -7251,17 +7357,42 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; + + /* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 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); - draw_notif_center(list_scroll, 1); - meow_dirty_full(); meow_ui_flush(); + 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 need_paint = 0; + int animating = anim_ok && (anim_p != anim_target); /* 自动息屏(同设置页语义) */ if (meow_auto_slept && !meow_screen_on) { @@ -7295,8 +7426,15 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; + /* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */ + 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); @@ -7376,6 +7514,9 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) /* 触摸 */ 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) { @@ -7386,18 +7527,49 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; - /* 按下原点落在滑条带 → 开始拖动 */ - if (drag_slider == 0) { - if (ncp_slider_band(tlx, tly, NCP_SL1_Y)) { drag_slider = 1; ncp_slider_apply(1, tlx, 0); } - else if (ncp_slider_band(tlx, tly, NCP_SL2_Y)) { drag_slider = 2; ncp_slider_apply(2, tlx, 0); } + 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) { ncp_slider_apply(1, tlx, 0); need_paint = 1; } - else if (drag_slider == 2 && td_ok) { 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; } + /* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 + + * 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式); + * 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */ + if (open_settled && td_ok && !scroll_drag && drag_slider == 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; } + 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; + } } else if (!val && td) { td = 0; if (drag_slider) { /* 滑条拖动结束 → 落盘 */ @@ -7406,9 +7578,29 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } 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)) { closed = 1; break; } - if (adx < 60 && ady < 60) { /* tap */ + /* 左缘左滑 = 关闭(复用全局左缘返回语义;动画中同样可打断)—— + * 优先于列表滚动落定,保「左缘返回关」语义不受滚动影响 */ + if (meow_edge_back(tdx, dx, dy)) { + if (anim_ok) anim_target = 0; else closed = 1; + break; + } + 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 (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++) @@ -7416,18 +7608,26 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) if (ncp_chip_apply(c)) need_sb = 1; /* 主题变化 → 重刷状态栏 */ hit = 1; need_paint = 1; break; } - if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { ncp_slider_apply(1, tx, 1); hit = 1; need_paint = 1; } - else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { ncp_slider_apply(2, tx, 1); hit = 1; need_paint = 1; } + 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; } if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) { if (ncp_row_tap(tx, ty, list_scroll, want_launch)) { hit = 1; - closed = 1; /* 点击通知:dismiss + 记录启动 → 关面板 */ + if (anim_ok) anim_target = 0; /* 点击通知:dismiss + 记录启动 → 滑出收回 */ + else closed = 1; break; } } - if (!hit) { 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)) { - closed = 1; break; /* 面板内上滑 → 关闭 */ + 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; /* 收回途中顶部热区下滑 → 重新打开(平滑转向) */ } /* 其余横滑/下滑忽略(不关闭不动作) */ } @@ -7437,7 +7637,7 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } } } - if (closed) break; /* 上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */ + if (closed) break; /* 非动画兜底:上滑 / 点空白 / 左缘左滑 / 通知点击 → 关面板返回 run_menu */ /* 列表变化检测:~500ms 对比 count;变化重采集 + 重绘列表区 */ if (now - last_listchk >= 500) { @@ -7451,16 +7651,63 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } } + /* 开关动画推进(§9:状态时间轴 + 快照 blit 平移;present 尊重 SDK 帧率门控,动画不更高频)。 + * 每 tick 依壁钟差推进 p;改 anim_target 即平滑转向(可打断,不跳变)。 */ + if (anim_ok && anim_p != anim_target) { + 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; + int off = mnp_anim_off(anim_p, NCP_ANIM_FIX, g_ncp_ph); + int cur_bot = ncp_anim_rebuild(off, anim_prev_bot); + int maxb = (anim_prev_bot > cur_bot) ? anim_prev_bot : cur_bot; + anim_prev_bot = cur_bot; + /* 只刷面板矩形 + 新露出的条(不整屏;不含状态栏 0-27 带) */ + meow_dirty_add(0, NCP_TOP, fb_xres, maxb - NCP_TOP); + meow_ui_flush(); + } + } + if (anim_ok && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 → 真正退出子循环 */ + /* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。 * need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍 - * 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。 */ + * 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。 + * 动画进行中:不画稳态面板(会覆盖正在滑动的帧),need_paint 保持到到位后再补画。 */ if (need_paint) { last_act = now; - draw_notif_center(list_scroll, need_sb); - need_sb = 0; - 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(); + 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; + } } /* ~1s 状态栏时钟 */ if (now - last_tick >= 1000) { @@ -7473,6 +7720,7 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } if (tfd >= 0) close(tfd); kp_close(&kp_power); kp_close(&kp_keys); + ncp_snap_free(); /* 释放动画快照(退出即回收,不驻留) */ ncp_rows_free(); logmsg("notification center closed"); } diff --git a/patch_v2/meow_launcherd/meow_notif_panel.h b/patch_v2/meow_launcherd/meow_notif_panel.h index 4912891..34dbadb 100644 --- a/patch_v2/meow_launcherd/meow_notif_panel.h +++ b/patch_v2/meow_launcherd/meow_notif_panel.h @@ -59,6 +59,18 @@ static inline int mnp_slider_val_from_x(int px, int sx, int sw, int min, int max return v; } +/* 滑条值的「跳步」吸附:把 v 取到最近的 step 整数倍。 + * step<=1 原样返回(默认 audio.volume_step=1 ⇒ 连续,与设置页滑条逐值行为一致); + * step>1(可配 1-5)时吸附到最近整数倍,减少拖动逐值写 DAC/刷状态栏(音量滑条卡顿修复)。 + * 不处理负数(音量/亮度值域均 ≥0);结果应再经 mnp_brightness_clamp / vol_clamp。 */ +static inline int mnp_step_round(int v, int step) +{ + if (step <= 1) return v; + int r = ((v + step / 2) / step) * step; + if (r < 0) r = 0; + return r; +} + /* ---- 亮度 clamp ---- */ /* 亮度 clamp:下限 vmin(=display.brightness_min,≥1 防黑屏)、上限 100(防闪屏)。 @@ -103,7 +115,7 @@ static inline int mnp_row_at(int y, int list_top, int list_bot, int row_h) return r; } -/* 列表滚动 clainmp:把 scroll 钳到 [0, total-view]。content 高度=total(总行数*row_h)。 */ +/* 列表滚动 clamp:把 scroll 钳到 [0, total-view]。content 高度=total(总行数*row_h)。 */ static inline int mnp_scroll_clamp(int scroll, int total, int view) { if (total <= view) return 0; @@ -113,4 +125,72 @@ static inline int mnp_scroll_clamp(int scroll, int total, int view) return scroll; } +/* ---- 列表拖动滚动(方向遵循 input.swipe_invert,与 launcher 设置页列表同式)---- */ + +/* 最大滚动位移:total=内容总高、view=可视区高。内容不足视口时恒 0(不滚动)。 */ +static inline int mnp_scroll_max(int total, int view) +{ + return (total > view) ? (total - view) : 0; +} + +/* 进入滚动时锁定的 scroll 基准(保证触发那一刻不跳变,连续跟手)。 + * scroll=进入时 scroll;d=进入时位移(当前 y-原点 y,屏幕 y 向下为正); + * invert=0(标准:手指上滑→scroll 增大=内容向上滚看后面项)映射 -d;invert=1(反向)映射 +d。 + * 与设置页 run_settings_page 的 `scroll_base = scroll_off + (invert ? -ddy : ddy)` 同式。 */ +static inline int mnp_scroll_base(int scroll, int d, int invert) +{ + return scroll + (invert ? -d : d); +} + +/* 由基准 + 当前位移推目标 scroll(含方向映射);结果交 mnp_scroll_clamp 钳到 [0,max]。 + * 与设置页 `scroll_off = clamp(scroll_base + (invert ? dydt : -dydt))` 同式。 */ +static inline int mnp_scroll_target(int base, int d, int invert) +{ + return base + (invert ? d : -d); +} + +/* 是否已越过滚动边界成「回弹」(拖到顶/底仍继续拖)——用于列表滚满后继续拖 → 关闭。 + * target=未钳位目标 scroll;max=mnp_scroll_max(...);tol=回弹容差(px,超过才认, + * 避免在边界小幅抖动即误关)。返回 1=已越过边界(回弹,可触发关闭语义)。 */ +static inline int mnp_scroll_rebound(int target, int max, int tol) +{ + return (target < -tol || target > max + tol); +} + +/* ---- 开关平移动画(状态时间轴,纯逻辑,可单测)---- + * 通知中心面板「打开/关闭滑入/收回」= 面板内容整体做 y-offset 平移(参照 SDK §5.2 快照 blit 平移)。 + * 状态:progress p ∈ [0, fix],0=全隐藏(面板内容在面板区上缘之外),fix=到位(offset_y=0)。 + * 本组函数只做标量时间轴/裁切计算,不碰 fb/几何——供 launcher(meow_launcherd.c 内嵌面板)与 + * 宿主单测(test_notif_panel.c)共用同一份实现,避免两端漂移。 */ + +/* 进度 p([0,fix])→ 面板内容 y-offset(px,∈[-ph,0])。 + * p=0 → -ph(内容上移出面板区,全隐藏);p=fix → 0(到位)。线性。 */ +static inline int mnp_anim_off(int p, int fix, int ph) +{ + return (int)((long long)(p - fix) * ph / fix); +} + +/* 沿时间轴单向推进:dt_ms(距上次 tick 的单调毫秒差)→ 新 p(向 target 逼近并 clamp)。 + * fix=进度分母(如 1024);dur_ms=单程时长(ms)。dt_ms<=0 或已达 target 时返回原 p。 + * 为保证单调前进(防快循环 dt 为 0 卡住),dt_ms>0 且未到位时 step 至少为 1。 + * 反转(p>target)即回退,同样逐帧平滑。 */ +static inline int mnp_anim_step(int p, int target, int fix, unsigned long long dt_ms, int dur_ms) +{ + if (dt_ms == 0) return p; + if (p == target) return p; + long long step = (long long)dt_ms * fix / (dur_ms > 0 ? dur_ms : 1); + if (step < 1) step = 1; + if (target > p) { p += (int)step; if (p > target) p = target; } + else { p -= (int)step; if (p < target) p = target; } + return p; +} + +/* 面板覆盖区底缘(screen y,exclusive):off 时面板可见区 = [top, top+ph+off)。 + * off∈[-ph,0] ⇒ 结果∈[top, top+ph]。top 通常=状态栏高(NCP_TOP=28,不盖状态栏)。 + * 调用方用「顶=top、高=covered_bot-top」作为脏区上缘/裁切范围。 */ +static inline int mnp_anim_covered_bot(int top, int ph, int off) +{ + return top + ph + off; +} + #endif /* MEOW_NOTIF_PANEL_H */ diff --git a/patch_v2/meow_launcherd/test_notif_panel.c b/patch_v2/meow_launcherd/test_notif_panel.c index 2355798..ed40165 100644 --- a/patch_v2/meow_launcherd/test_notif_panel.c +++ b/patch_v2/meow_launcherd/test_notif_panel.c @@ -96,6 +96,18 @@ static void test_pure_logic(void) CHECK(mnp_slider_val_from_x(0, 20, 440, 1, 100) == 1, "slider x out-left clamped to min"); CHECK(mnp_slider_val_from_x(999, 20, 440, 1, 100) == 100, "slider x out-right clamped to max"); + /* mnp_step_round:跳步吸附(音量滑条去抖/减少逐值写 DAC) */ + CHECK(mnp_step_round(37, 1) == 37, "step_round step=1 -> unchanged (continuous, same as settings slider)"); + CHECK(mnp_step_round(0, 1) == 0, "step_round step=1 -> 0 kept"); + CHECK(mnp_step_round(37, 5) == 35, "step_round 37 step=5 -> 35 (nearest multiple of 5)"); + CHECK(mnp_step_round(38, 5) == 40, "step_round 38 step=5 -> 40 (rounds up to nearest)"); + CHECK(mnp_step_round(50, 5) == 50, "step_round 50 step=5 -> exactly on step"); + CHECK(mnp_step_round(0, 5) == 0, "step_round 0 step=5 -> 0"); + CHECK(mnp_step_round(100, 5) == 100, "step_round 100 step=5 -> 100"); + CHECK(mnp_step_round(99, 5) == 100, "step_round 99 step=5 -> 100 (rounds up to max)"); + CHECK(mnp_step_round(2, 4) == 4, "step_round 2 step=4 -> 4 (round to nearest multiple)"); + CHECK(mnp_step_round(-3, 5) == 0, "step_round negative clamps to 0 (value domain >=0)"); + /* mnp_brightness_clamp */ CHECK(mnp_brightness_clamp(0, 1) == 1, "brightness clamp below min -> min"); CHECK(mnp_brightness_clamp(150, 1) == 100, "brightness clamp above 100 -> 100"); @@ -126,6 +138,106 @@ static void test_pure_logic(void) CHECK(mnp_scroll_clamp(5, 400, 444) == 0, "content fits view -> 0"); } +/* P4 通知列表滚动:方向映射(input.swipe_invert)+ 回弹判定(纯逻辑,header-only)。 + * 语义与设置页列表一致:invert=0 手指上滑(d<0) → target 增大(内容上滚看后面项); + * invert=1(反向)手指上滑 → target 减小。进滚动锁 base 保证触发瞬间不跳变。 */ +static void test_scroll_pure(void) +{ + fprintf(stderr, "--- P4 notification list scroll (swipe_invert direction + rebound) ---\n"); + int total = 2000, view = 444, max_scroll = total - view; /* 1556 */ + + /* mnp_scroll_max */ + CHECK(mnp_scroll_max(2000, 444) == 1556, "scroll_max overflow -> total-view"); + CHECK(mnp_scroll_max(400, 444) == 0, "scroll_max content fits -> 0"); + CHECK(mnp_scroll_max(444, 444) == 0, "scroll_max equal -> 0"); + CHECK(mnp_scroll_max(445, 444) == 1, "scroll_max 1px over -> 1"); + + /* mnp_scroll_base:进滚动锁基准(invert=0 同设置页 `scroll + d`;invert=1 反向) */ + CHECK(mnp_scroll_base(100, -45, 0) == 55, "base invert=0 finger-up -> scroll+d"); + CHECK(mnp_scroll_base(100, -45, 1) == 145, "base invert=1 finger-up -> scroll-d"); + + /* mnp_scroll_target:触发瞬间 target 回到原 scroll(连续跟手,不跳变) */ + CHECK(mnp_scroll_target(55, -45, 0) == 100, "target invert=0 at engage -> back to scroll"); + CHECK(mnp_scroll_target(145, -45, 1) == 100, "target invert=1 at engage -> back to scroll"); + + /* 方向映射:invert=0 手指持续上滑(更负) → target 增大;invert=1 → target 减小 */ + CHECK(mnp_scroll_target(55, -200, 0) == 255, "target invert=0 finger-up more -> increases"); + CHECK(mnp_scroll_target(145, -200, 1) == -55, "target invert=1 finger-up more -> decreases"); + CHECK(mnp_scroll_target(55, 200, 0) == -145, "target invert=0 finger-down -> decreases"); + CHECK(mnp_scroll_target(145, 200, 1) == 345, "target invert=1 finger-down -> increases"); + + /* clamp 落位:进滚动 + 拖到边界 → 钳到 [0,max_scroll] */ + CHECK(mnp_scroll_clamp(mnp_scroll_target(55, -200, 0), total, view) == 255, "clamp invert=0 up -> in range"); + CHECK(mnp_scroll_clamp(mnp_scroll_target(145, -200, 1), total, view) == 0, "clamp invert=1 up -> bottom(0)"); + CHECK(mnp_scroll_clamp(mnp_scroll_target(55, 200, 0), total, view) == 0, "clamp invert=0 down -> top(0)"); + + /* mnp_scroll_rebound:边内容差 tol=48;界内不触发,越过才触发 */ + CHECK(mnp_scroll_rebound(0, max_scroll, 48) == 0, "rebound at top(0) in-tol -> 0"); + CHECK(mnp_scroll_rebound(-47, max_scroll, 48) == 0, "rebound top overshoot 47<48 -> 0"); + CHECK(mnp_scroll_rebound(-49, max_scroll, 48) == 1, "rebound top overshoot 49>48 -> 1"); + CHECK(mnp_scroll_rebound(max_scroll, max_scroll, 48) == 0, "rebound at bottom in-tol -> 0"); + CHECK(mnp_scroll_rebound(max_scroll + 47, max_scroll, 48) == 0, "rebound bottom overshoot 47<48 -> 0"); + CHECK(mnp_scroll_rebound(max_scroll + 49, max_scroll, 48) == 1, "rebound bottom overshoot 49>48 -> 1"); + CHECK(mnp_scroll_rebound(500, max_scroll, 48) == 0, "rebound mid-range -> 0"); +} + +/* P3 开关平移动画状态机:p 推进/翻转/中断、offset 计算、裁切边界、关闭到 0 才退出。 + * 用任意 fix/ph 参数化(默认 1024/692,通知中心面板 PH=720-28=692);纯函数不依赖 fb。 */ +static void test_anim_pure(void) +{ + fprintf(stderr, "--- P3 open/close slide animation state machine ---\n"); + enum { FIX = 1024, PH = 692 }; + + /* mnp_anim_off:p→offset 线性映射(∈[-PH,0];p=0 全隐藏 / p=FIX 到位) */ + CHECK(mnp_anim_off(0, FIX, PH) == -PH, "anim_off p=0 -> -PH (fully hidden above)"); + CHECK(mnp_anim_off(FIX, FIX, PH) == 0, "anim_off p=FIX -> 0 (settled open)"); + CHECK(mnp_anim_off(FIX / 2, FIX, PH) == -(PH / 2), "anim_off mid -> -PH/2"); + + /* mnp_anim_step:打开推进(0→FIX) */ + CHECK(mnp_anim_step(0, FIX, FIX, 180, 180) == FIX, "step dt=dur -> reaches FIX in one tick"); + CHECK(mnp_anim_step(0, FIX, FIX, 90, 180) == FIX / 2, "step dt=dur/2 -> half progress"); + CHECK(mnp_anim_step(0, FIX, FIX, 10, 180) >= 1, "step small dt -> at least 1 (monotonic, no stall)"); + CHECK(mnp_anim_step(0, FIX, FIX, 0, 180) == 0, "step dt=0 -> no move"); + CHECK(mnp_anim_step(0, FIX, FIX, 100000, 180) == FIX, "step huge dt -> clamped at FIX"); + + /* 反向推进(关闭:FIX→0,线性步进率 = dt*fix/dur,非剩余比例) */ + CHECK(mnp_anim_step(FIX, 0, FIX, 90, 180) == FIX / 2, "step close dt=dur/2 -> half back"); + CHECK(mnp_anim_step(FIX / 2, 0, FIX, 45, 180) == FIX / 4, "step reversal mid -> toward 0 linearly"); + CHECK(mnp_anim_step(FIX / 4, 0, FIX, 100000, 180) == 0, "step reversal huge dt -> clamped at 0"); + + /* 打断(打开中→关闭→再打开):平滑转向,不跳变到目标 */ + int p = mnp_anim_step(0, FIX, FIX, 90, 180); /* 打开到半程 (512) */ + int p_before = p; + p = mnp_anim_step(p, 0, FIX, 30, 180); /* 打断:转为关闭,小步推进 */ + CHECK(p < p_before && p > 0, "interrupt open->close: heading to 0 (not target), mid value ok"); + int s2 = 0; + while (s2++ < 1000 && p != 0) p = mnp_anim_step(p, 0, FIX, 30, 180); + CHECK(p == 0, "interrupt close completes at 0"); + p = mnp_anim_step(p, FIX, FIX, 180, 180); /* 关闭到位后再打开 */ + CHECK(p == FIX, "reopen after interrupt -> settled open"); + + /* mnp_anim_covered_bot:裁切边界(off∈[-PH,0] → bot∈[top, top+PH]) */ + CHECK(mnp_anim_covered_bot(28, PH, -PH) == 28, "covered_bot fully hidden -> top (statusbar bottom)"); + CHECK(mnp_anim_covered_bot(28, PH, 0) == 28 + PH, "covered_bot settled -> top+PH"); + CHECK(mnp_anim_covered_bot(28, PH, -(PH / 2)) == 28 + PH / 2, "covered_bot halfway -> top+PH/2"); + CHECK(mnp_anim_covered_bot(28, PH, 0) <= 720 && mnp_anim_covered_bot(28, PH, -PH) >= 28, + "covered_bot within [28,720] — 不盖状态栏、不出屏底"); + + /* 完整关闭:多次 tick 才到 0;单调不增;且只在 p==0 才算退出 */ + p = FIX; + int steps = 0, prev = p, monotonic = 1; + while (steps++ < 100000 && p != 0) { + int np = mnp_anim_step(p, 0, FIX, 10, 180); + if (np > prev) monotonic = 0; /* 关闭方向必须单调减小 */ + if (np < 0 || np > FIX) monotonic = 0; + p = np; prev = np; + } + CHECK(p == 0, "close animation terminates at p=0 (exit condition)"); + CHECK(steps > 1, "close takes multiple ticks (smooth sliding, not a jump)"); + CHECK(monotonic, "close progress monotonic non-increasing"); + CHECK(mnp_anim_step(0, 0, FIX, 10, 180) == 0, "at p==0 with target 0 stays put (would break)"); +} + static void test_storage_contract(void) { fprintf(stderr, "--- P0 storage contract (panel reads) ---\n"); @@ -227,6 +339,8 @@ int main(void) meow_notif_set_tf_max(1024 * 1024); test_pure_logic(); + test_scroll_pure(); + test_anim_pure(); test_storage_contract(); unlink(g_tflog); rmdir(dir);