通知中心 ⑧⑨: 一键清除(保留活动通知) + 面板跟手下拉半程可回(agent-133)

- ⑧ 一键清除: mnp_clearable(ongoing/media/progress) 红线保留活动通知; 先 meow_notif 转储(notifications.log+10MB滚动) 再清空显示; 勿扰左半+清除右半(NCP_DND_SPLIT_X=fb_xres/2)对称, 命中互斥
- ⑨ 面板跟手下拉/半程可回: reveal 连续映射 1:1 跟手(mnp_drag_progress/reveal), 松手 mnp_drag_anchor(fix,40%)=1→补到1024/0→回0收回; 与⑦行左滑/列表滚动/meow_edge_back 互斥; 复用快照+局部刷(panel帧/新露出条)
- meow_ui.a 零改动(d249c6e6); launcher=b140e9e5; 宿主189项ok; 新增0warn
- NOTIFICATION_SYSTEM §9 增 ⑧⑨; MEOW_APP_SDK §5.7 收合动画落点标注
This commit is contained in:
MeowOS committed 2026-08-30 17:45:15 +08:00
1 parent 82b77d64b4
commit dafe31784d
6 files changed
+406 -22

No files matched your search

+1 -1
View File
@@ -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。
- **关闭**:点面板外关闭 / 再划回边缘收合 / 返回键关闭(模态时)。
+2
View File
@@ -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
Binary file not shown.
+224 -21
View File
@@ -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<SB_BAR_H) + 下拉达 NCP_DRAG_ENTER_SLOP →
* 立即切入 run_notif_center(此时手指仍按下拖动),由面板内拖动路径把 progress
* 映射为手指位移(跟手 1:1),松手由面板内锚定判定补到展开/收回(半程可回)。
* 原「抬手边沿 mnp_top_swipe_down」打开改为「拖动中即开」(该抬手边沿保留为防御)。
* 先在本次 ABS 内触发一次(menu_live_open_pending latch);
* 进入前 t_down=0 使返回后同次手势的剩余事件被忽略(防 double-open/残留)。 */
if (t_down && t_down_valid && !menu_live_open_pending &&
t_down_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<MEOW_EDGE_W)起点交给 meow_edge_back 优先消费;
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
logmsg("menu: top-edge swipe-down -> 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) {
@@ -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 */
+132
View File
@@ -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);