diff --git a/MEOW_APP_SDK.md b/MEOW_APP_SDK.md index a0d52eb..53bfbdf 100644 --- a/MEOW_APP_SDK.md +++ b/MEOW_APP_SDK.md @@ -435,7 +435,7 @@ void meow_ui_dialog_close(meow_dialog *d); - **底部边缘上滑** → **底部面板**(本模板新增;底部边缘无既有冲突)。 - **快捷设置**:为不与通知中心冲突,**待定案**——建议从通知中心二级拉出、或屏幕右上角/右侧缘单独热区触达、或「通知中心内点箭头展开」。**不新占顶部下滑**(避免与 §5.1 冲突)。 - **侧边栏**(可选):右缘向左滑出(右侧无既有手势,安全)。 -- **收合动画**:跟手平移面板(手指拖多长露多长),松手**过半阈值** → 补 2-4 帧**短插值**吸附到位;**未过** → 回弹。动画只在松手时做一次,不做逐帧几何重算(B1 无 tween;短插值=有限帧序列)。 +- **收合动画**:跟手平移面板(手指拖多长露多长),松手**过半阈值** → 补 2-4 帧**短插值**吸附到位;**未过** → 回弹。动画只在松手时做一次,不做逐帧几何重算(B1 无 tween;短插值=有限帧序列)。`[落点 2026-08-30]`:launcher 通知中心(顶部面板)已按此模板实现「跟手下拉 + 松手锚定」(progress=手指位移映射,40% 阈值补到位/回弹收回,复用快照 blit 平移,不逐帧重排)——见 NOTIFICATION_SYSTEM §9(⑨)与 devdocs NOTIF_CENTER_UI_PLAN §16。 - **内容懒载**:面板**首次拉出**才构建内容(或预建空容器),避免打开即全量绘制;面板内滚动局部 dirty。 - **关闭**:点面板外关闭 / 再划回边缘收合 / 返回键关闭(模态时)。 diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md index 9e8c676..3cca280 100644 --- a/NOTIFICATION_SYSTEM.md +++ b/NOTIFICATION_SYSTEM.md @@ -73,6 +73,8 @@ target_activity(可选, 来自 hints) / progress(可选 int) / urgency(0-3, 缺 - **交互**:**状态栏下滑展开**(用户已定);**上滑 / 点面板外关**;**左缘返回关**(与全局左缘返回/顶部滑条按起始位置分流,见 spec-ref §5.1)。列表显示通知(app 图标 + summary + body + target Activity),点击按路由表解析跳转(M3 导航栈),下拉关闭/清除。 - **⑥ 快捷 chip + 面板内开关(launcher 侧,用户拍板 2026-08-30)**:快捷行为 **5 个**(WiFi/蓝牙/亮度/主题/**电源菜单**——第 5 个 chip 点击**唤起既有系统电源菜单** `show_power_menu`,真弹对话框);**勿扰不做 chip**,改**通知中心面板内开关**(列表上方小 toggle),开=只显示 `urgency>=2`(HIGH/CRITICAL),状态存 `notify.dnd`(SYSTEM_SETTINGS §1),面板内读写 `meow_settings_*` 零旁路。 - **⑦ 单条通知左滑删除(launcher 侧,用户拍板 2026-08-30)**:通知行**从右往左横向滑动删除**,跟手 + 半程可回(未过阈值/反向滑回 → 回弹复原);行内左滑=删该条、竖向=列表滚动、`meow_edge_back`(左缘起点)优先不变,滑动只在该行生效;松手过阈值 → `meow_notif_dismiss` + 行移除/重排。 +- **⑧ 一键清除通知(launcher 侧,用户拍板 2026-08-30)**:勿扰开关带**对称处(右半段)**加「一键清除」按钮(垃圾桶字形 + 清除),点击把**可清除的非活动通知先转储留档**(走既有 dump 链 `meow_notif_dismiss → dump_cb → meow_notif_dump_tf`,一行 CSV 追加到 `MeowOS/notifications/notifications.log` + 10MB 滚动),再**清空显示**(对可清除项 dismiss,活动项保留)。**红线:绝不清「活动状态」通知**——① ongoing 常驻、② 媒体通知(媒体会话单例)、③ 带进度条进行中的通知(`progress>=0`;终态 100% 已由 `meow_notif_update_progress` 里程碑落盘释放,不再活跃)一律保留;只清「非 ongoing / 非 media / 无进行中进度(`progress<0`)」的普通通知。= 转储留档先、清空显示后,活动项照常显示。 +- **⑨ 面板「跟手下拉、半程可回」(launcher 侧,用户拍板 2026-08-30)**:面板打开/关闭由「触发式 180ms 时间轴过渡」升级为**手指拖动实时跟手**——手指在顶部热区下拉时 `progress = 手指下移量映射`(复用 `mnp_anim_off`,progress∈[0,1024],跟手 1:1),松手 `progress ≥ 40%×1024≈410` → 走时间轴补到 1024(完成展开);`< 阈值 或 反向拖回` → 回弹到 0 收回(半程取消)。关闭同理可跟手:上滑拖动时 progress 1→0 跟手,松手 `< 阈值(回到接近 1)` → 回弹重新展开;`≥ 阈值` → 时间轴归 0 收回。与行左滑(⑦ 横向主导)/列表滚动(§11 列表溢出区)/`meow_edge_back`(左缘)互斥分流,竖向滑动仍是列表滚动/关闭语义。 - **协议/约束**:遵守 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 diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index c619435..bf303a7 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 c48c635..d6d8bcd 100644 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ b/patch_v2/meow_launcherd/meow_launcherd.c @@ -3290,7 +3290,8 @@ static void run_settings_page(void); /* 阶段B:通知中心面板(定义在下方 run_settings_page 之后;run_menu 顶部热区下滑调用)。 * *want_launch 初始为 -1;若点击通知命中可启动 app,返回时置为 apps[] 下标(run_menu 据此 * 走标准启动链路)。返回后由调用方重置按键防抖基线防面板误切换。 */ -static void run_notif_center(struct app *apps, int napps, int *want_launch); +static void run_notif_center(struct app *apps, int napps, int *want_launch, + int live_open, int lox, int loy); static int touch_fd = -1; static int t_last_x = -1, t_last_y = -1; @@ -3300,7 +3301,16 @@ static int t_down_x = 0, t_down_y = 0; * 故 t_down_x/y 延迟到「本次按下之后」的首个有效 ABS 才锁定,避免把上一手势残留 * (-1/旧坐标) 当本次原点。t_down_valid=0 表示本次尚未锁到有效坐标。 */ static int t_down_valid = 0; +/* ⑨ 顶部热区「跟手下拉」进入通知中心的 per-gesture latch:进入后置 1,本次手势不再二次进入; + * 由下次 touch-down 复位。防止「拖动中切入 run_notif_center 后返回」把同次手势再判成打开/滑动。 */ +static int menu_live_open_pending = 0; #define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */ +/* ⑨ 面板跟手下拉、半程可回(progress=手指位移映射;复用 mnp_anim_off/快照/ncp_anim_rebuild)。 + * NCP_DRAG_ENTER_SLOP = 顶部热区下拉进入 run_notif_center(run_menu 切入时机)的位移阈值(px); + * NCP_DRAG_THR_PCT = 松手锚定阈值(progress >= fix*40/100≈410 → 补到展开;否则收回=半程取消)。 + * 拖满=面板高 PH(g_ncp_ph),实现「面板顶缘 1:1 跟手」:off = dy - PH。 */ +#define NCP_DRAG_ENTER_SLOP 16 +#define NCP_DRAG_THR_PCT 40 /* Bug 2/4 (2026-08-28): after an app exits we return to the menu, but touch * events that were generated DURING the app run (the goodix ghost touch the * v6.4 watchdog guards against, plus the back-tap/press that actually exited @@ -4407,6 +4417,52 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, if (t_down && !t_down_valid && t_last_x >= 0 && t_last_y >= 0) { t_down_x = t_last_x; t_down_y = t_last_y; t_down_valid = 1; } + /* ⑨ 顶部热区「跟手下拉」打开通知中心(拖动中即进,真跟手): + * 原点锁定 + 起点在顶部热区(y= 0 && t_down_y < SB_BAR_H && + (t_last_y - t_down_y) >= NCP_DRAG_ENTER_SLOP) { + menu_live_open_pending = 1; /* 本次手势仅触发一次 */ + int loy = t_down_y; + logmsg("menu: top-edge live drag-down -> notification center (dy=%d)", + t_last_y - loy); + t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */ + menu_arm_touch(TAP_ARM_DELAY_MS); + int want_launch = -1; + run_notif_center(apps, napps, &want_launch, 1, t_down_x, loy); + /* 重播按键防抖基线(同原 top-swipe 路径),防面板内电源/音量边沿被 run_menu 误判 */ + 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); + last_input_ms = now_ms(); + menu_arm_touch(800); + dirty = 1; + if (want_launch >= 0 && want_launch < napps) { + logmsg("menu: notification launch app idx=%d", want_launch); + int ret = launch_app(&apps[want_launch]); + menu_after_app_exit(ret == 0); + last_input_ms = now_ms(); + dirty = 1; + if (ret != 0) { + snprintf(toast, sizeof toast, "CANNOT START: %s", + apps[want_launch].name); + toast_ms = TOAST_MS; + } + } + continue; /* 本圈不再按 tap/其余处理(剩余事件因 t_down=0 成 no-op) */ + } } else if (type == EV_KEY && code == BTN_TOUCH) { if (val && !t_down) { /* Bug 2/4: ignore touches inside the post-app @@ -4422,6 +4478,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, t_down = 1; t_down_valid = 0; /* 原点待首个有效坐标锁定(防残留 -1/旧值) */ t_last_x = -1; t_last_y = -1; /* 本次以「无坐标」起步(防跨手势残留) */ + menu_live_open_pending = 0; /* ⑨ 新触摸:复位 live-open latch */ } } else if (!val && t_down) { @@ -4442,10 +4499,10 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, * 左缘(down_x notification center"); + logmsg("menu: top-edge swipe-down -> notification center (release edge, fallback)"); menu_arm_touch(TAP_ARM_DELAY_MS); int want_launch = -1; - run_notif_center(apps, napps, &want_launch); + run_notif_center(apps, napps, &want_launch, 0, 0, 0); /* ⑨ live_open=0 → 触发式自动展开(防御) */ /* 重播按键防抖基线,防面板内电源/音量边沿被 run_menu 误判(M2.25 同款)。 */ memset(&kd_pwr, 0, sizeof kd_pwr); memset(&kd_vold, 0, sizeof kd_vold); @@ -6809,6 +6866,10 @@ done: /* 勿扰过滤阈值:dnd=1 只显示 urgency>=2(HIGH=2/CRITICAL=3) */ #define NCP_DND_MIN_URGENCY 2 +/* ⑧ 一键清除(勿扰对称处):与勿扰带同排(NCP_DND_Y/H),x 对半(中线=fb_xres/2)。 + * 勿扰=左半段 [NCP_ROW_X, fb_xres/2);清除=右半段 [fb_xres/2, fb_xres-NCP_ROW_X)。 */ +#define NCP_DND_SPLIT_X (fb_xres / 2) /* 勿扰/清除带分割中线(左右对称) */ + /* ---- 开关平移动画(§9:状态时间轴 + 快照 blit 平移,不整屏重绘)---- */ #define NCP_ANIM_FIX 1024 /* 进度 p 的定点分母(1/1024 精度) */ #define NCP_ANIM_MS 180 /* 单程打开/关闭时长(ms,真机可调) */ @@ -7015,6 +7076,21 @@ static int ncp_anim_rebuild(int off, int prev_bot) 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) { @@ -7260,14 +7336,26 @@ static void ncp_repaint_row(int scroll, int row, int off) meow_ui_flush(); } -/* ⑥ 勿扰开关带命中(面板内小 toggle):落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 整宽 → 命中。 - * 不与 5 chip(y≤130)、滑条带(y≤238)、通知列表(y≥NCP_LIST_TOP=308)、状态栏(y<28)重叠。 */ +/* ⑥/⑧ 勿扰开关带命中(面板内小 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 < fb_xres); + return (ty >= NCP_DND_Y && ty < NCP_DND_Y + NCP_DND_H && + tx >= 0 && tx < NCP_DND_SPLIT_X); } -/* ⑥ 绘勿扰带:label「勿扰」+ 铃铛图标 + 状态「开/关」 + 右侧 s_draw_switch(48×28)。 */ +/* ⑧ 一键清除带命中(勿扰对称处的右半段): + * 落在 [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; @@ -7285,8 +7373,52 @@ static void ncp_draw_dnd(const meow_ui_theme_t *th) 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); - /* 右侧开关(既有的开关绘制;48×28 垂直居中) */ - s_draw_switch(fb_xres - 24 - 48, cy - 14, dnd_on, th); + /* 左半段右侧开关(⑧:从原 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); +} + +/* ---- ⑧ 一键清除(批量):先转储留档 → 再清空显示(红线:保留 ongoing/media/progress)---- */ +typedef struct { uint32_t ids[NCP_MAX_ROWS]; int n; } ncp_clear_ctx_t; +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 时另绘状态栏在最顶)。 @@ -7366,8 +7498,10 @@ static void draw_notif_center(int scroll, int redraw_sb) /* 分隔线 */ fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline); - /* ⑥ 勿扰面板内开关带(list 上方) */ + /* ⑥ 勿扰面板内开关带(list 上方,左半段) */ ncp_draw_dnd(th); + /* ⑧ 一键清除按钮(勿扰对称处的右半段) */ + ncp_draw_clear(th); /* 通知列表 */ ncp_draw_list(scroll); @@ -7443,7 +7577,8 @@ static int ncp_row_tap(int tx, int ty, int scroll, int *want_launch) } /* ---- 通知中心面板主循环(阻塞;参照 run_settings_page 交互惯用法)---- */ -static void run_notif_center(struct app *apps, int napps, int *want_launch) +static 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; @@ -7488,6 +7623,15 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; @@ -7504,6 +7648,15 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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); @@ -7576,6 +7729,7 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; @@ -7713,6 +7867,30 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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(与设置页列表同式); * 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */ @@ -7750,6 +7928,8 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; @@ -7793,6 +7973,14 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } 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; @@ -7837,6 +8025,19 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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; @@ -7871,10 +8072,11 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) last_listchk = now; meow_notif_tick(); if (ncp_rows_changed()) { - /* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。 */ + /* ⑦ 列表已变(新条/撤下/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); @@ -7884,7 +8086,7 @@ 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) { + if (anim_ok && anim_p != anim_target && !panel_drag) { unsigned long long anow = now_ms(); int dt; if (anim_target != anim_last_target) { @@ -7901,16 +8103,10 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) (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(); + ncp_anim_frame(anim_p, &anim_prev_bot, anim_ok); /* §9 快照 blit 平移(只刷面板矩形) */ } } - if (anim_ok && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 → 真正退出子循环 */ + 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)。 @@ -7976,6 +8172,13 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) 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) { diff --git a/patch_v2/meow_launcherd/meow_notif_panel.h b/patch_v2/meow_launcherd/meow_notif_panel.h index a254b7e..1dafab8 100644 --- a/patch_v2/meow_launcherd/meow_notif_panel.h +++ b/patch_v2/meow_launcherd/meow_notif_panel.h @@ -244,4 +244,51 @@ static inline int mnp_swipe_step(int off, int target, unsigned long long dt_ms, return off + dir * (int)step; } +/* ---- ⑧ 一键清除通知判定(纯逻辑,可单测)---- + * 通知中心「一键清除」= 把可清除的非活动通知先转储留档(走既有 dump 链),再清空显示; + * 红线(源:用户 2026-08-30):① ongoing 常驻、② 媒体通知(媒体会话单例)、③ 带进度条进行中的通知 + * (progress>=0;终态 100% 已由 meow_notif_update_progress 里程碑落盘释放,不再活跃)——一律保留。 + * 本函数只做「该不该清」的标量判定,不碰存储/fb;ongoing/media/progress 直读 meow_notif_t 字段。 */ + +/* 「可清除」判定:非 ongoing 且 非 media 且 无进行中进度(progress<0)。返回 1=应清除;0=保留(红线)。 */ +static inline int mnp_clearable(int ongoing, int media, int progress) +{ + return !ongoing && !media && progress < 0; +} + +/* ---- ⑨ 面板「跟手下拉、半程可回」(纯逻辑,可单测)---- + * 把面板打开/关闭从「触发式时间轴过渡」升级为「手指拖动实时跟手」:progress∈[0,fix], + * reveal(面板当前露出高度,∈[0,ph])随手指位移连续映射;松手按阈值锚定补到位/收回。 + * 本组函数只做标量:reveal→progress / seed+delta clamp / 松手锚定,不碰 fb/几何—— + * 供 launcher(meow_launcherd.c 内嵌面板)与宿主单测(test_notif_panel.c)共用同一份实现。 */ + +/* 跟手 reveal→progress:reveal∈[0,ph](面板露出高度)→ p∈[0,fix]。线性、方向无关。 + * 与 mnp_anim_off 互逆(off = -ph + reveal;p = fix*reveal/ph)。 */ +static inline int mnp_drag_progress(int reveal, int fix, int ph) +{ + if (reveal <= 0) return 0; + if (reveal >= ph) return fix; + return (int)((long long)reveal * fix / ph); +} + +/* reveal 跟手 clamp:seed(进入拖动时锁定的露出高) + delta(自进入竖向位移, y 向下为正) → clamp[0,ph]。 + * 连续可反向:反向 delta 减小 reveal,未到边界不跳变(半程可回)。ph<=0 时恒 0。 */ +static inline int mnp_drag_reveal(int seed, int delta, int ph) +{ + if (ph <= 0) return 0; + int r = seed + delta; + if (r < 0) r = 0; + if (r > ph) r = ph; + return r; +} + +/* 松手锚定:progress p(∈[0,fix])与阈值(fix*thr_pct/100,如 40%→410)相比。 + * 返回 1=≥阈值 → 前去 fix(打开完成 / 关闭手势未过阈值→回弹重新展开); + * 返回 0=<阈值 或 反向拖回 → 回 0(开半程取消收回 / 关闭过阈值→收回)。 */ +static inline int mnp_drag_anchor(int p, int fix, int thr_pct) +{ + int thr = fix * thr_pct / 100; + return p >= thr ? 1 : 0; +} + #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 8c45bc1..4ffd7ab 100644 --- a/patch_v2/meow_launcherd/test_notif_panel.c +++ b/patch_v2/meow_launcherd/test_notif_panel.c @@ -311,6 +311,136 @@ static void test_dnd_swipe_pure(void) CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: -100 back under threshold -> cancel"); } +/* P6 ⑧ 一键清除判定 + ⑨ 面板跟手下拉/半程可回(纯逻辑,header-only): + * - mnp_clearable:红线(ongoing / media / progress>=0 进行中)一律保留;非活动普通通知可清。 + * - mnp_drag_progress:reveal→progress(0/fix/线性/越界 clamp)。 + * - mnp_drag_reveal:seed+delta 正向/反向/边界 clamp(连续、半程可回)。 + * - mnp_drag_anchor:松手锚定(≥40%→前去 fix;<40%→回 0)。 + * 另加 mini 状态机:开(跟手下拉→松手过阈值展开/未过收回)+ 关(上滑跟手→松手未过阈值回弹重展/过阈值收回)。 */ +static void test_clear_drag_pure(void) +{ + fprintf(stderr, "--- P6 clear predicate + panel drag-follow ---\n"); + enum { FIX = 1024, PH = 692 }; + + /* mnp_clearable */ + CHECK(mnp_clearable(1, 0, -1) == 0, "clearable: ongoing -> keep"); + CHECK(mnp_clearable(0, 1, -1) == 0, "clearable: media -> keep"); + CHECK(mnp_clearable(0, 0, 50) == 0, "clearable: progress in-progress(50) -> keep"); + CHECK(mnp_clearable(0, 0, 0) == 0, "clearable: progress bar(0) -> keep"); + CHECK(mnp_clearable(0, 0, -1) == 1, "clearable: plain inactive -> clear"); + CHECK(mnp_clearable(0, 1, 50) == 0, "clearable: media+progress -> keep"); + CHECK(mnp_clearable(1, 0, 50) == 0, "clearable: ongoing+progress -> keep"); + + /* mnp_drag_progress(reveal∈[0,ph] → p∈[0,fix]) */ + CHECK(mnp_drag_progress(0, FIX, PH) == 0, "drag_progress reveal=0 -> 0"); + CHECK(mnp_drag_progress(PH, FIX, PH) == FIX, "drag_progress reveal=ph -> fix"); + CHECK(mnp_drag_progress(PH / 2, FIX, PH) == FIX / 2, "drag_progress mid -> half"); + CHECK(mnp_drag_progress(-50, FIX, PH) == 0, "drag_progress negative reveal -> 0"); + CHECK(mnp_drag_progress(PH + 50, FIX, PH) == FIX, "drag_progress reveal>ph -> fix"); + CHECK(mnp_drag_progress(173, FIX, 692) == FIX * 173 / 692, "drag_progress linear 173/692"); + + /* mnp_drag_reveal(seed+delta → [0,ph],连续/反向/边界) */ + CHECK(mnp_drag_reveal(0, 0, PH) == 0, "drag_reveal seed0 delta0 -> 0"); + CHECK(mnp_drag_reveal(0, 300, PH) == 300, "drag_reveal open: seed0+300 -> 300 (1:1)"); + CHECK(mnp_drag_reveal(PH, -300, PH) == PH - 300, "drag_reveal close: seed ph -300"); + CHECK(mnp_drag_reveal(100, -200, PH) == 0, "drag_reveal clamps below 0 (reverse past top)"); + CHECK(mnp_drag_reveal(100, 1000, PH) == PH, "drag_reveal clamps above ph (drag past bottom)"); + CHECK(mnp_drag_reveal(0, -50, PH) == 0, "drag_reveal reverse on hidden stays 0"); + CHECK(mnp_drag_reveal(50, 0, 0) == 0, "drag_reveal ph<=0 -> 0"); + + /* mnp_drag_anchor(≥fix*40/100=409 → 1;<409 → 0) */ + CHECK(mnp_drag_anchor(409, FIX, 40) == 1, "drag_anchor at 40% (409) -> go open"); + CHECK(mnp_drag_anchor(410, FIX, 40) == 1, "drag_anchor above 40% -> go open"); + CHECK(mnp_drag_anchor(1024, FIX, 40) == 1, "drag_anchor full -> go open"); + CHECK(mnp_drag_anchor(408, FIX, 40) == 0, "drag_anchor below 40% -> collapse"); + CHECK(mnp_drag_anchor(0, FIX, 40) == 0, "drag_anchor 0 -> collapse"); + CHECK(mnp_drag_anchor(0, FIX, 0) == 1, "drag_anchor thr_pct=0 -> always open"); + + /* mini 状态机:跟手下拉打开(seed 从0 升)→ 松手锚定 */ + int seed = 0, p = 0; + int dy = 0; + /* 手指下移 300px:reveal=seed+(tly-drag_y0) */ + seed = 0; p = mnp_drag_progress(mnp_drag_reveal(seed, 300, PH), FIX, PH); + CHECK(p > 0 && p < FIX, "mini open-follow: drag 300px -> partial progress"); + CHECK(mnp_drag_anchor(p, FIX, 40) == 1, "mini open-follow: 300/692=43% >= 40% -> open"); + /* 反向拖回 200(reveal=200,即拖 200)→ 半程取消(29% < 40%) */ + p = mnp_drag_progress(mnp_drag_reveal(seed, 200, PH), FIX, PH); + CHECK(p < FIX && mnp_drag_anchor(p, FIX, 40) == 0, "mini open-follow: 200/692=29% < 40% -> collapse(cancel)"); + /* 全拉满 → 满 */ + CHECK(mnp_drag_anchor(mnp_drag_progress(mnp_drag_reveal(seed, PH, PH), FIX, PH), FIX, 40) == 1, + "mini open-follow: drag full -> open"); + /* 关闭:seed=PH 上滑 600 → reveal=PH-600 → 过阈值收回 */ + int close_p = mnp_drag_progress(mnp_drag_reveal(PH, -600, PH), FIX, PH); + CHECK(close_p < FIX && mnp_drag_anchor(close_p, FIX, 40) == 0, "mini close-follow: up 600 -> close"); + /* 关闭:上滑仅 150(reveal=PH-150,仍高)→ 松手回弹重展 */ + int reopen_p = mnp_drag_progress(mnp_drag_reveal(PH, -150, PH), FIX, PH); + CHECK(reopen_p > 0 && mnp_drag_anchor(reopen_p, FIX, 40) == 1, "mini close-follow: up 150 -> reopen"); +} + +/* P7 ⑧ 一键清除(存储契约):先用 mnp_clearable + meow_notif_list/dismiss 模拟面板 ncp_clear_all, + * 验证「先转储留档(dismiss 触发 dump_cb)→ 清出显示」且红线项(ongoing/media/progress)保留。 */ +typedef struct { uint32_t ids[8]; int n; } clear_ctx_t; +static int clear_collect_cb(const meow_notif_t *n, void *ctx) +{ + clear_ctx_t *c = ctx; + if (!mnp_clearable(n->ongoing, n->media, n->progress)) return 0; + if (c->n >= 8) return 1; + c->ids[c->n++] = n->id; + return 0; +} +/* 通用「收集全部活跃 id」(兼作测试排空用:非仅可清)。 */ +static int clear_all_cb(const meow_notif_t *n, void *ctx) +{ + clear_ctx_t *c = ctx; + if (c->n >= 8) return 1; + c->ids[c->n++] = n->id; + return 0; +} +static void test_clear_storage(void) +{ + fprintf(stderr, "--- P7 clear-all storage contract (dump then clear, keep ongo/media/progress) ---\n"); + /* 排空上一测试留下的所有活跃通知,保证本测试从干净基数开始(count 断言可用绝对值)。 */ + for (int pass = 0; pass < 8 && meow_notif_count(NULL) > 0; pass++) { + clear_ctx_t c2; memset(&c2, 0, sizeof c2); + meow_notif_list(NULL, clear_all_cb, &c2); /* clear_all_cb:收集全部(非仅可清) */ + if (c2.n == 0) break; + for (int i = 0; i < c2.n; i++) meow_notif_dismiss(c2.ids[i]); + } + meow_notif_t n; memset(&n, 0, sizeof n); + /* 普通(可清) */ + snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general"); + snprintf(n.summary, sizeof n.summary, "普通1"); n.progress = -1; n.urgency = 1; + uint32_t idA = 0; CHECK(meow_notif_post(&n, &idA) == NOTIF_OK, "clear st: post plain1"); + /* 普通2(可清) */ + memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general"); + snprintf(n.summary, sizeof n.summary, "普通2"); n.progress = -1; n.urgency = 1; + uint32_t idB = 0; CHECK(meow_notif_post(&n, &idB) == NOTIF_OK, "clear st: post plain2"); + /* 媒体(红线:保留) */ + memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "media"); + snprintf(n.summary, sizeof n.summary, "媒体"); n.ongoing = 1; n.media = 1; n.progress = 30; n.urgency = 2; + uint32_t idM = 0; CHECK(meow_notif_post(&n, &idM) == NOTIF_OK, "clear st: post media"); + /* 进度进行中(红线:保留) */ + memset(&n, 0, sizeof n); + snprintf(n.app_id, sizeof n.app_id, "os.meow.app"); snprintf(n.category, sizeof n.category, "general"); + snprintf(n.summary, sizeof n.summary, "下载中"); n.progress = 60; n.urgency = 2; + uint32_t idP = 0; CHECK(meow_notif_post(&n, &idP) == NOTIF_OK, "clear st: post progress"); + CHECK(meow_notif_count(NULL) == 4, "clear st: count 4 before clear"); + + /* 按 mnp_clearable 收集 + dismiss(= dump_cb + 释放槽,模拟 ncp_clear_all) */ + clear_ctx_t c; memset(&c, 0, sizeof c); + meow_notif_list(NULL, clear_collect_cb, &c); + int cleared = 0; + for (int i = 0; i < c.n; i++) { meow_notif_dismiss(c.ids[i]); cleared++; } + CHECK(cleared == 2, "clear st: exactly 2 plain cleared"); + CHECK(meow_notif_count(NULL) == 2, "clear st: 2 remaining after clear (media+progress kept)"); + grab_list(); + CHECK(find_slot("媒体") != NULL, "clear st: media kept"); + CHECK(find_slot("下载中") != NULL, "clear st: progress kept"); + CHECK(find_slot("普通1") == NULL && find_slot("普通2") == NULL, "clear st: plain dismissed"); +} + static void test_storage_contract(void) { fprintf(stderr, "--- P0 storage contract (panel reads) ---\n"); @@ -415,7 +545,9 @@ int main(void) test_scroll_pure(); test_anim_pure(); test_dnd_swipe_pure(); + test_clear_drag_pure(); test_storage_contract(); + test_clear_storage(); unlink(g_tflog); rmdir(dir);