diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md index f7b291b..9e8c676 100644 --- a/NOTIFICATION_SYSTEM.md +++ b/NOTIFICATION_SYSTEM.md @@ -71,6 +71,8 @@ target_activity(可选, 来自 hints) / progress(可选 int) / urgency(0-3, 缺 - **渲染主体 = 系统进程(meow_launcherd),非 APP**:通知中心 UI 由系统进程负责渲染(复用 SDK 控件/状态栏基底);**APP 进程不渲染通知中心**(系统级控件仅内部,见 MEOW_APP_SDK §6.1 ③、FRAMEWORK_DESIGN §10.2)。 - **数据来源**:通知列表经 IPC 从通知存储/通知服务读(如 `MSG_NOTIFY_LIST` 重同步、`target_activity`/`progress`/`icon` 等字段),launcher 只做 UI 呈现与手势,不各画各的。 - **交互**:**状态栏下滑展开**(用户已定);**上滑 / 点面板外关**;**左缘返回关**(与全局左缘返回/顶部滑条按起始位置分流,见 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` + 行移除/重排。 - **协议/约束**:遵守 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/SYSTEM_SETTINGS.md b/SYSTEM_SETTINGS.md index c9dd9ab..6f4c6a6 100644 --- a/SYSTEM_SETTINGS.md +++ b/SYSTEM_SETTINGS.md @@ -50,6 +50,7 @@ | | `notify.categories` | int | 4 | 通知类型数上限(每 App,M2.53)[源:审计 2026-08-30]| | | `notify.category_quota` | int | 4 | 每类型活动通知上限(4×4=16/App)| | | `notify.media_quota` | int | 1 | 媒体通知单例(每 App 仅 1 条)| +| | `notify.dnd` | bool | false | 通知中心「勿扰」面板内开关(通知列表上方小 toggle;开=只显示 `urgency>=2`(HIGH/CRITICAL),关=全部;面板内读写 `meow_settings_*`,零旁路)[源:用户 2026-08-30] | | **日志** | `log.level` | enum | `info` | `error/warn/info/debug` | | | `log.to_card` | bool | true | 写 TF 卡(无卡回退内存环) | | **音频** | `audio.volume` | int 0-100 | 60 | 系统音量(MeowOS 直写 CS43198 DAC;读回为追踪值) | diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index ee8bfbe..c619435 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 db40965..c48c635 100644 --- a/patch_v2/meow_launcherd/meow_launcherd.c +++ b/patch_v2/meow_launcherd/meow_launcherd.c @@ -6779,13 +6779,15 @@ done: #define NCP_CHIP_H 44 #define NCP_CHIP_W 70 #define NCP_CHIP_GAP 8 -#define NCP_CHIP_N 4 +#define NCP_CHIP_N 5 /* ⑥:快捷 chip 扩充为 5 个(+电源菜单) */ #define NCP_SLIDER_X 20 #define NCP_SLIDER_W (fb_xres - 40) #define NCP_SL1_Y 162 /* 亮度滑条 track 顶(label 在 -26 =136) */ #define NCP_SL2_Y 214 /* 音量滑条 track 顶(label 在 -26 =188) */ #define NCP_DIV_Y 258 -#define NCP_LIST_TOP 266 +#define NCP_DND_Y 264 /* ⑥:勿扰面板内开关带顶缘(分隔线下方) */ +#define NCP_DND_H 40 /* 勿扰开关带高(label+状态+s_draw_switch 48×28) */ +#define NCP_LIST_TOP 308 /* 通知列表顶缘(原 266 → 下移 42 给勿扰带) */ #define NCP_LIST_BOT 694 #define NCP_ROW_H 92 #define NCP_ROW_X 12 @@ -6798,13 +6800,22 @@ done: #define NCP_SCROLL_REBOUND_TOL 48 /* px:列表拖到顶/底仍继续拖过此容差 → 判回弹(关闭语义), 避免在边界小幅抖动即误关(> S_SCROLL_SLOP=40) */ +/* ---- ⑦ 单条通知行左滑删除(跟手 + 半程可回 + 回弹)---- */ +#define NCP_SWIPE_SLOP 24 /* px:行内水平位移达此值且横向主导+左移 → 进行左滑(>阈值) */ +#define NCP_SWIPE_RATE 3 /* px/ms:行左滑落位动画线性速率(回弹/滑出) */ +#define NCP_SWIPE_DELETE_FRAC 40 /* %:行宽百分比阈值(│off│>=row_w*40/100 → 删) */ +#define NCP_SWIPE_DELETE_MIN 110 /* px:绝对像素阈值(│off│>=110 → 删;OR 语义,窄行由 frac 起作用) */ + +/* 勿扰过滤阈值:dnd=1 只显示 urgency>=2(HIGH=2/CRITICAL=3) */ +#define NCP_DND_MIN_URGENCY 2 + /* ---- 开关平移动画(§9:状态时间轴 + 快照 blit 平移,不整屏重绘)---- */ #define NCP_ANIM_FIX 1024 /* 进度 p 的定点分母(1/1024 精度) */ #define NCP_ANIM_MS 180 /* 单程打开/关闭时长(ms,真机可调) */ -/* chip 索引 */ +/* chip 索引(⑥:第 5 个 = 电源菜单,点击唤起既有系统电源菜单 show_power_menu) */ enum { NCP_CHIP_WIFI=0, NCP_CHIP_BT=1, NCP_CHIP_THEME=2, NCP_CHIP_BRIGHT=3, - NCP_CHIP_COUNT=4 }; + NCP_CHIP_POWER=4, NCP_CHIP_COUNT=5 }; /* 通知行快照(面板读取字段 + 缓存的 icon 句柄)。 */ typedef struct { @@ -6825,6 +6836,17 @@ static int g_nrows = 0; static struct app *g_ncp_apps = NULL; /* run_notif_center 注入(通知点击找 app 用) */ static int g_ncp_napps = 0; +/* ⑥ 勿扰面板内开关:当前 dnd 态(开面板/切换时从 meow_settings notify.dnd 读/写,零旁路)。 + * ncp_collect_cb / ncp_active_count 按它过滤行(only urgency>=NCP_DND_MIN_URGENCY when on)。 */ +static int g_ncp_dnd = 0; + +/* ⑦ 行左滑删除的绘制态(供 ncp_draw_list / ncp_repaint_row 画该行位移)。 + * g_ncp_swipe_row = 正在左滑的行下标(-1=无);g_ncp_swipe_off = 该行左移量(px,<=0)。 + * run_notif_center 的手势状态机(row_swipe_row/row_off/row_settle/row_off_target/row_anim_at/row_paint) + * 与其同步,绘制只读这组全局,避免给 draw_notif_center/ncp_draw_list 加参数。 */ +static int g_ncp_swipe_row = -1; +static int g_ncp_swipe_off = 0; + /* 短串拷贝(同 meow_notif.c 语义:保证 NUL + 截断) */ static void ncp_strncpy(char *dst, const char *src, size_t len) { @@ -6859,6 +6881,8 @@ static int ncp_collect_cb(const meow_notif_t *n, void *ctx) { (void)ctx; if (g_nrows >= NCP_MAX_ROWS) return 1; /* 满:中止遍历 */ + /* 勿扰(⑥):dnd 开时只收集 urgency>=NCP_DND_MIN_URGENCY(HIGH/CRITICAL)。 */ + if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0; ncp_row_t *r = &g_rows[g_nrows++]; memset(r, 0, sizeof *r); r->id = n->id; @@ -6890,12 +6914,25 @@ static int ncp_refresh_rows(void) return g_nrows; } -/* 通知列表是否变化(简单按活跃数比;新条/撤下/TTL 释放都反映在 count)。 */ +/* 当前应显示的活跃通知数(按 g_ncp_dnd 过滤,与 g_rows 同口径)——供变化检测/重采集用。 */ +static int ncp_count_cb(const meow_notif_t *n, void *ctx) +{ + if (!mnp_dnd_should_show(n->urgency, g_ncp_dnd)) return 0; + (*(int *)ctx)++; + return 0; +} +static int ncp_active_count(void) +{ + int c = 0; + meow_notif_list(NULL, ncp_count_cb, &c); + if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS; + return c; +} + +/* 通知列表是否变化(按过滤后活跃数比;新条/撤下/TTL 释放都反映在 count,含 dnd 过滤)。 */ static int ncp_rows_changed(void) { - int c = meow_notif_count(NULL); - if (c > NCP_MAX_ROWS) c = NCP_MAX_ROWS; - return c != g_nrows; + return ncp_active_count() != g_nrows; } /* ---- 开关平移动画辅助(§9:快照 blit + 平移;只在 run_notif_center 内分配/释放)---- @@ -7057,11 +7094,16 @@ 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_POWER) { + /* 电源菜单 chip:非开关(无 on/off 态),恒示中性;点击由 ncp_chip_apply 唤起电源菜单。 */ + *on = 0; } } /* chip 快捷动作(写 meow_settings_* + 侧效应;零手写文件)。 - * 返回 1=主题变化(需重刷状态栏);0=其它。 */ + * 返回 1=主题变化(需重刷状态栏);2=电源菜单已弹出(调用方置 after_modal=1 整帧重绘还原); + * 0=其它。电源菜单 chip 复用既有系统级 show_power_menu(含 dim/圆钮/音量导航/电源确认/8s强关/ + * 空闲自动息屏),真弹对话框,动作在函数内 system();取消则返回续 panel —— 与电源键长按同款。 */ static int ncp_chip_apply(int chip) { if (chip == NCP_CHIP_WIFI) { @@ -7098,12 +7140,72 @@ static int ncp_chip_apply(int chip) g_sys.brightness = vmin; } paint_sb_right(); + } else if (chip == NCP_CHIP_POWER) { + /* 电源菜单 chip:唤起既有系统级电源菜单(真弹对话框,动作在函数内 system();取消则返回)。 + * 不涉及任何设置项(电源菜单不走 meow_settings,零旁路)。返回 2 → 调用方整帧重绘还原面板/状态栏。 */ + logmsg("notif-center: power chip -> show power menu"); + show_power_menu(); + return 2; } meow_settings_save(); return 0; } -/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。 */ +/* ⑦ 画删除底:行右缘被空出的条(宽=-off)铺 error 红底 + 白「删除」字(条宽≥44 才显字,否则纯红渐进)。 + * off<=0(负得多=卡滑出多、露出右条多);off=0 不画。 */ +static void ncp_draw_delete_bg(int ry, int off, const meow_ui_theme_t *th) +{ + if (off >= 0) return; + int bw = -off; + if (bw <= 0) return; + int bx = NCP_ROW_X + NCP_ROW_W + off; /* 被空出的右条左缘 */ + fill_rect(bx, ry, bw, NCP_ROW_H - 6, th->error); + if (bw >= 44) { + int cx = bx + bw / 2, cy = ry + (NCP_ROW_H - 6) / 2; + ui_draw(cx - ui_width("删除", 16) / 2, cy - meow_font_line_height(16) / 2, + "删除", 0xFFFFFF, 16); + } +} + +/* 画一行内容(行卡 + 图标/APP名/时间/标题/正文/进度),整体随 off 左移(⑦ 行左滑跟手)。 + * i=g_rows 下标;ry=行顶(已含 scroll);off=行左移量(px,<=0)。 */ +static void ncp_draw_row(int i, int ry, int off, const meow_ui_theme_t *th) +{ + const ncp_row_t *r = &g_rows[i]; + meow_gfx_fill_round(NCP_ROW_X + off, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container); + /* 图标 */ + int icx = NCP_ICON_X + off, icy = ry + (NCP_ROW_H - 32) / 2; + if (r->img) meow_ui_img_cover(icx, icy, 32, r->img); + else { + /* 回退:主题色圆点 + 首字符 */ + const char *nm = ncp_app_name(r->app_id); + ncp_dot(icx + 16, icy + 16, 16, th->primary); + char ch = nm[0] ? nm[0] : '?'; + char cbuf[4] = { ch, 0, 0, 0 }; + /* 首字符若是多字节(中文),取整码 */ + if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0; + if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } } + int cw = ui_width(cbuf, 20); + ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20); + } + /* app 名 + 时间 */ + ui_draw(NCP_TEXT_X + off, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16); + char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf); + ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16) + off, ry + 12, tbuf, th->on_surface_variant, 16); + /* 标题 + 内容 */ + ncp_text(NCP_TEXT_X + off, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W); + ncp_text(NCP_TEXT_X + off, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W); + /* 进度(progress>=0):细条 */ + if (r->progress >= 0) { + int px = NCP_TEXT_X + off, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14; + fill_rect(px, py, pw, 4, th->surface_variant); + int fw = r->progress * pw / 100; + if (fw > 0) fill_rect(px, py, fw, 4, th->primary); + } +} + +/* 绘制通知列表可见区(scroll=像素偏移)。每行:图标 + app名 + 标题 + 内容 + 时间 + 进度。 + * ⑦:左滑中的行(g_ncp_swipe_row)按 g_ncp_swipe_off 左移,其下先画删除底(ncp_draw_delete_bg)。 */ static void ncp_draw_list(int scroll) { const meow_ui_theme_t *th = s_ui_theme(); @@ -7116,37 +7218,9 @@ static void ncp_draw_list(int scroll) for (int i = 0; i < g_nrows; i++) { int ry = NCP_LIST_TOP + i * NCP_ROW_H - scroll; if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) continue; /* 视口外整行跳过 */ - const ncp_row_t *r = &g_rows[i]; - meow_gfx_fill_round(NCP_ROW_X, ry, NCP_ROW_W, NCP_ROW_H - 6, 12, th->surface_container); - /* 图标 */ - int icx = NCP_ICON_X, icy = ry + (NCP_ROW_H - 32) / 2; - if (r->img) meow_ui_img_cover(icx, icy, 32, r->img); - else { - /* 回退:主题色圆点 + 首字符 */ - const char *nm = ncp_app_name(r->app_id); - ncp_dot(icx + 16, icy + 16, 16, th->primary); - char ch = nm[0] ? nm[0] : '?'; - char cbuf[4] = { ch, 0, 0, 0 }; - /* 首字符若是多字节(中文),取整码 */ - if ((unsigned char)ch >= 0x80 && nm[1]) { cbuf[1] = nm[1]; cbuf[2] = 0; - if ((unsigned char)ch >= 0xE0 && nm[2]) { cbuf[2] = nm[2]; cbuf[3] = 0; } } - int cw = ui_width(cbuf, 20); - ui_draw(icx + 16 - cw / 2, icy + 16 - meow_font_line_height(20) / 2, cbuf, 0xFFFFFF, 20); - } - /* app 名 + 时间 */ - ui_draw(NCP_TEXT_X, ry + 10, ncp_app_name(r->app_id), th->on_surface_variant, 16); - char tbuf[32]; mnp_time_ago(r->created_ms, (uint64_t)time(NULL) * 1000u, tbuf, sizeof tbuf); - ui_draw(NCP_ROW_X + NCP_ROW_W - 8 - ui_width(tbuf, 16), ry + 12, tbuf, th->on_surface_variant, 16); - /* 标题 + 内容 */ - ncp_text(NCP_TEXT_X, ry + 30, r->summary, 20, th->on_surface, NCP_TEXT_W); - ncp_text(NCP_TEXT_X, ry + 56, r->body, 18, th->on_surface_variant, NCP_TEXT_W); - /* 进度(progress>=0):细条 */ - if (r->progress >= 0) { - int px = NCP_TEXT_X, pw = NCP_TEXT_W, py = ry + NCP_ROW_H - 14; - fill_rect(px, py, pw, 4, th->surface_variant); - int fw = r->progress * pw / 100; - if (fw > 0) fill_rect(px, py, fw, 4, th->primary); - } + int off = (i == g_ncp_swipe_row) ? g_ncp_swipe_off : 0; /* 左滑中的行位移 */ + if (off < 0) ncp_draw_delete_bg(ry, off, th); + ncp_draw_row(i, ry, off, th); } /* 滚动条 */ if (total > NCP_LIST_BOT - NCP_LIST_TOP) { @@ -7170,6 +7244,51 @@ static void ncp_repaint_list(int scroll) meow_ui_flush(); } +/* ⑦ 仅重绘一行(行左滑跟手/回弹/滑出专用;只刷该行矩形,不整面板、不刷状态栏)。 + * 清该行带(含被滑出的右条)→ 画删除底(若 off<0)+ 该行内容位移 → 只标该行带脏。 */ +static void ncp_repaint_row(int scroll, int row, int off) +{ + const meow_ui_theme_t *th = s_ui_theme(); + if (row < 0 || row >= g_nrows) return; + int ry = NCP_LIST_TOP + row * NCP_ROW_H - scroll; + if (ry + NCP_ROW_H < NCP_LIST_TOP || ry >= NCP_LIST_BOT) return; /* 视口外:无需重绘 */ + /* 清该行带全宽(含左缘 [0,NCP_ROW_X),因深滑时卡可滑到左缘外),再画删除底 + 位移行卡。 */ + fill_rect(0, ry, NCP_ROW_X + NCP_ROW_W, NCP_ROW_H, th->surface_variant); + if (off < 0) ncp_draw_delete_bg(ry, off, th); + ncp_draw_row(row, ry, off, th); + meow_dirty_add(0, ry, fb_xres, NCP_ROW_H); + meow_ui_flush(); +} + +/* ⑥ 勿扰开关带命中(面板内小 toggle):落在 [NCP_DND_Y, NCP_DND_Y+NCP_DND_H) 整宽 → 命中。 + * 不与 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); +} + +/* ⑥ 绘勿扰带:label「勿扰」+ 铃铛图标 + 状态「开/关」 + 右侧 s_draw_switch(48×28)。 */ +static void ncp_draw_dnd(const meow_ui_theme_t *th) +{ + int y = NCP_DND_Y; + int dnd_on = g_ncp_dnd; + int cy = y + NCP_DND_H / 2; + /* 铃铛(简化:圆顶 + 顶蒂 + 底部小圆点)。 */ + int bx = NCP_ROW_X + 4, bcy = cy - 2; + meow_gfx_fill_round(bx, bcy - 7, 16, 13, 7, th->on_surface_variant); /* 铃身圆顶 */ + meow_gfx_fill_round(bx + 6, bcy - 11, 4, 4, 2, th->on_surface_variant);/* 顶蒂 */ + ncp_dot(bx + 8, bcy + 6, 2, th->on_surface_variant); /* 底部小圆点 */ + /* label + 状态 */ + ui_draw(bx + 22, cy - meow_font_line_height(18) / 2 + 2, "勿扰", th->on_surface, 18); + char st[16]; + snprintf(st, sizeof st, "%s", dnd_on ? "开" : "关"); + int lw = ui_width("勿扰", 18); + ui_draw(bx + 22 + lw + 12, cy - meow_font_line_height(18) / 2 + 2, st, + dnd_on ? th->primary : th->on_surface_variant, 18); + /* 右侧开关(既有的开关绘制;48×28 垂直居中) */ + s_draw_switch(fb_xres - 24 - 48, cy - 14, dnd_on, th); +} + /* 绘制整个通知中心面板(画进 SDK back_buf;redraw_sb=1 时另绘状态栏在最顶)。 * 性能:面板体单独;(0..27) 状态栏只在打开/主题变化时 redraw_sb=1 重绘,交互时跳过, * 使脏区仅限面板体,绝不整屏重绘。 */ @@ -7191,8 +7310,8 @@ static void draw_notif_center(int scroll, int redraw_sb) ui_draw(24, NCP_TITLE_Y, "通知中心", th->on_surface, 22); ui_draw(W - 24 - ui_width("点空白关闭", 16), NCP_TITLE_Y + 2, "点空白关闭", th->on_surface_variant, 16); - /* 快捷开关行 */ - static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度" }; + /* 快捷开关行(⑥:5 个 = WiFi/蓝牙/主题/亮度/电源菜单,居中自动随 NCP_CHIP_N) */ + static const char *chip_lbl[NCP_CHIP_COUNT] = { "WiFi", "蓝牙", "主题", "亮度", "电源菜单" }; int total_chip = NCP_CHIP_N * NCP_CHIP_W + (NCP_CHIP_N - 1) * NCP_CHIP_GAP; int chip_x0 = (W - total_chip) / 2; for (int c = 0; c < NCP_CHIP_N; c++) { @@ -7210,6 +7329,13 @@ 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_POWER) { + /* 电源符号:圆环 + 顶部短柄,近似 ⏻(用现有 meow_gfx_fill_round 近似圆)。 */ + int pc = icon_cx + SB_ICON_SIZE / 2; + meow_gfx_fill_round(pc - 8, icon_cy - 8, 16, 16, 8, fg); /* 外环 */ + meow_gfx_fill_round(pc - 4, icon_cy - 4, 8, 8, 4, bg); /* 内孔(显环厚) */ + meow_gfx_fill_round(pc - 1, icon_cy - 16, 3, 9, 1, fg); /* 顶部短柄 */ + } char label[24]; snprintf(label, sizeof label, "%s", chip_lbl[c]); if (val[0] && c == NCP_CHIP_BRIGHT) @@ -7240,6 +7366,9 @@ static void draw_notif_center(int scroll, int redraw_sb) /* 分隔线 */ fill_rect(12, NCP_DIV_Y, W - 24, 1, th->outline); + /* ⑥ 勿扰面板内开关带(list 上方) */ + ncp_draw_dnd(th); + /* 通知列表 */ ncp_draw_list(scroll); @@ -7349,6 +7478,16 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) int scroll_invert = 0; /* input.swipe_invert(0=标准方向) */ int scroll_rebound = 0; /* 拖到顶/底仍继续拖(回弹)→ 抬手按关闭语义收起 */ int list_paint = 0; /* 仅列表区重绘(滚动专用,不整面板/不刷状态栏) */ + /* ⑦ 单条通知行左滑删除(跟手+半程可回+回弹;绘制态见 g_ncp_swipe_row/off): + * row_swipe_row = 正在左滑的行下标(-1=无);row_off = 行左移量(px,<=0,跟手); + * row_settle = 0 跟手 / 1 松手后向 row_off_target 回弹或滑出;row_anim_at = 落位动画计时。 + * row_paint = 仅重绘该行带(跟手局部 blit/位移,不整面板/不刷状态栏)。 */ + int row_swipe_row = -1; + int row_off = 0; + int row_settle = 0; + int row_off_target = 0; + unsigned long long row_anim_at = 0; + int row_paint = 0; /* 仅重绘该行带(行左滑专用) */ int closed = 0; /* 非动画路径的兜底关面板(快照不可用时瞬时退出) */ int after_modal = 0; /* 电源菜单返回:整帧重绘还原被 dim 的状态栏 */ int wake_suppress = 0; @@ -7365,6 +7504,12 @@ 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; + /* ⑥ 读勿扰面板内开关态(开面板一次;切换在下面 tap 分支写回)。 */ + { + int d = 0; meow_settings_get_bool("notify.dnd", &d); + g_ncp_dnd = d; + } + /* 首次绘制:先拍背景(应用列表)→ 渲染最终位置整面板 → 拍面板快照 → 还原背景到 p=0 * 起点(面板隐藏、先露应用列表),随后进入循环由动画推进 p:0→FIX 滑入。 */ ncp_refresh_rows(); @@ -7428,6 +7573,9 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) td = 0; td_ok = 0; drag_slider = 0; tlx = tly = -1; scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; scroll_invert = 0; scroll_rebound = 0; list_paint = 0; + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; /* 唤醒:面板直接就位(不重播滑入动画,面板早已打开);重拍快照防内容已变 */ anim_p = anim_target = anim_last_target = NCP_ANIM_FIX; if (anim_ok) anim_prev_bot = NCP_TOP + g_ncp_ph; @@ -7536,10 +7684,39 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } if (drag_slider == 1 && td_ok) { if (ncp_slider_apply(1, tlx, 0)) need_paint = 1; } else if (drag_slider == 2 && td_ok) { if (ncp_slider_apply(2, tlx, 0)) need_paint = 1; } + /* ⑦ 行左滑(进入):面板就位(open_settled)+ 未锁其它手势 + 位移达 slop, + * 判定横向主导(adx>ady)且左移(ddx<0)且起点落在可见行 → 进行左滑(只该行)。 + * 与竖向滚动互斥:本分支锁 row_swipe_row 后下方 scroll 不再进入(gate 含 + * row_swipe_row<0);竖向主导(ady>adx)或不左移则回退既有滚动(改变不大: + * 以前「横主导左移」也滚,现改为行左滑,符合「横向主导→行左滑」)。 */ + if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) { + int ddx = tlx - tdx, ddy = tly - tdy; + int adx = ddx < 0 ? -ddx : ddx, ady = ddy < 0 ? -ddy : ddy; + if ((adx >= NCP_SWIPE_SLOP || ady >= S_SCROLL_SLOP) && adx > ady && ddx < 0) { + int eff = tdy - NCP_LIST_TOP + list_scroll; + int row = mnp_row_at(eff, 0, g_nrows * NCP_ROW_H, NCP_ROW_H); + /* 起点必须在列表区(DND 带/底部提示等不在列表区,防 scroll 导致误匹行) */ + if (tdy >= NCP_LIST_TOP && tdy < NCP_LIST_BOT && + row >= 0 && row < g_nrows) { + row_swipe_row = row; + row_off = mnp_swipe_clamp(ddx, NCP_ROW_W); + row_settle = 0; row_off_target = 0; + row_anim_at = now_ms(); + g_ncp_swipe_row = row; g_ncp_swipe_off = row_off; + row_paint = 1; + } + } + } + /* ⑦ 行左滑跟手:已进则更新 off(反向可回 → 回落至 0);只该行生效。 */ + if (row_swipe_row >= 0 && td_ok && !row_settle) { + row_off = mnp_swipe_clamp(tlx - tdx, NCP_ROW_W); + g_ncp_swipe_off = row_off; + row_paint = 1; + } /* 列表滚动(§11):面板就位(open_settled)+ 起点在列表区 + 内容溢出 + * 纵向位移达 slop → 进滚动。方向遵循 input.swipe_invert(与设置页列表同式); * 拖到顶/底继续拖 → 回弹标记。动画进行中(面板在滑,未就位)不启动滚动。 */ - if (open_settled && td_ok && !scroll_drag && drag_slider == 0) { + if (open_settled && td_ok && !scroll_drag && drag_slider == 0 && row_swipe_row < 0) { int ddy = tly - scroll_y0; if (ddy >= S_SCROLL_SLOP || ddy <= -S_SCROLL_SLOP) { int total = g_nrows * NCP_ROW_H; @@ -7569,6 +7746,10 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) td = 1; td_ok = 0; tlx = -1; tly = -1; drag_slider = 0; scroll_drag = 0; scroll_base = 0; scroll_y0 = 0; scroll_invert = 0; scroll_rebound = 0; + /* ⑦ 新触摸:清行左滑(防上一手势残留) */ + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; } } else if (!val && td) { td = 0; @@ -7584,7 +7765,20 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) if (anim_ok) anim_target = 0; else closed = 1; break; } - if (scroll_drag) { + if (row_swipe_row >= 0) { + /* ⑦ 行左滑松手:过阈值 → 滑出删除;未过/反向 → 回弹复原(半程取消)。 + * 只该行生效;本手势锁定后不再走 tap/上滑关闭/点空白,不关面板。 */ + /* 用抬手最终位移兜底刷新 off(可能最后有位移但无 ABS 事件跟进)。 */ + row_off = mnp_swipe_clamp(dx, NCP_ROW_W); + g_ncp_swipe_off = row_off; + if (mnp_swipe_delete(row_off, NCP_ROW_W, + NCP_SWIPE_DELETE_FRAC, NCP_SWIPE_DELETE_MIN)) { + row_off_target = -NCP_ROW_W; /* 滑出(删除)目标:整行移出 */ + } else { + row_off_target = 0; /* 回弹复原 */ + } + row_settle = 1; row_anim_at = now_ms(); + } else if (scroll_drag) { /* 列表滚动落定:最终 clamp 到边界(拖动中已实时跟手,此处确认终值); * 若回弹(拖到顶/底继续拖)→ 按「上滑/下滑关闭」收起(§11.3)。 * 绝不把「滑列表」误判成「上滑关闭」——scroll_drag 一旦置位即跳过 @@ -7605,11 +7799,44 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) int hit = 0; for (int c = 0; c < NCP_CHIP_COUNT; c++) if (ncp_chip_hit(c, tx, ty)) { - if (ncp_chip_apply(c)) need_sb = 1; /* 主题变化 → 重刷状态栏 */ + int ar = ncp_chip_apply(c); + if (ar == 1) need_sb = 1; /* 主题变化 → 重刷状态栏 */ + else if (ar == 2) { + after_modal = 1; /* 电源菜单已弹 → 整帧重绘还原 */ + /* 电源菜单返回:菜单期间用户可能按电源确认/取消,键边沿可能错配, + * 同电源键长按路径(line 7480-7488)重 poll + 重采 m_prev_*,防误判「单击」→ 误切 panel。 */ + memset(&kd_pwr, 0, sizeof kd_pwr); + memset(&kd_vold, 0, sizeof kd_vold); + memset(&kd_volu, 0, sizeof kd_volu); + memset(&kd_play, 0, sizeof kd_play); + kp_poll(&kp_power, NULL, NULL); + kp_poll(&kp_keys, NULL, NULL); + m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); + m_prev_vold = kp_is_down(&kp_keys, KEY_VOLUMEDOWN); + m_prev_volu = kp_is_down(&kp_keys, KEY_VOLUMEUP); + m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); + /* 菜单期间用户的触摸经 evdev 广播也会进本 panel 的 tfd → 立即排空, + * 防返回后把「菜单里的点击」误判为 panel 内 tap/滑动。 */ + if (tfd >= 0) { + unsigned char ev2[16]; + while (read(tfd, ev2, sizeof ev2) == 16) {} + } + } hit = 1; need_paint = 1; break; } if (!hit && ncp_slider_band(tx, ty, NCP_SL1_Y)) { hit = 1; if (ncp_slider_apply(1, tx, 1)) need_paint = 1; } else if (!hit && ncp_slider_band(tx, ty, NCP_SL2_Y)) { hit = 1; if (ncp_slider_apply(2, tx, 1)) need_paint = 1; } + /* ⑥ 勿扰面板内开关(列表上方小 toggle):tap 即切换(整带命中) */ + if (!hit && ncp_dnd_hit(tx, ty)) { + int dv = 0; meow_settings_get_bool("notify.dnd", &dv); + meow_settings_set_bool("notify.dnd", dv ? 0 : 1); + meow_settings_save(); + g_ncp_dnd = dv ? 0 : 1; + ncp_refresh_rows(); /* 按 dnd 过滤重采集 */ + list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, + NCP_LIST_BOT - NCP_LIST_TOP); + hit = 1; need_paint = 1; + } if (!hit && ty >= NCP_LIST_TOP && ty < NCP_LIST_BOT) { if (ncp_row_tap(tx, ty, list_scroll, want_launch)) { hit = 1; @@ -7644,6 +7871,10 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) last_listchk = now; meow_notif_tick(); if (ncp_rows_changed()) { + /* ⑦ 列表已变(新条/撤下/TTL):取消当前行左滑,避免行下标错位。 */ + row_swipe_row = -1; row_off = 0; row_settle = 0; + row_off_target = 0; row_anim_at = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; ncp_refresh_rows(); list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, NCP_LIST_BOT - NCP_LIST_TOP); @@ -7681,6 +7912,42 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) } if (anim_ok && anim_target == 0 && anim_p == 0) break; /* 滑出到 0 → 真正退出子循环 */ + /* ⑦ 行左滑落位动画:row_settle=1 时按壁钟差向 row_off_target 线性步进(回弹→0 / 滑出→-row_w)。 + * 到达目标:0=回弹完成(行复原,无需重排);-row_w=删除 → dismiss + 重采集 + 重排(need_paint)。 + * 只该行生效;与面板开关动画(上块)互不干扰(行左滑仅在面板稳态 open_settled 下进行)。 */ + if (row_swipe_row >= 0 && row_settle) { + unsigned long long rnow = now_ms(); + int rdt = (int)((rnow > row_anim_at) ? (rnow - row_anim_at) : 0); + row_anim_at = rnow; + int noff = mnp_swipe_step(row_off, row_off_target, (unsigned long long)rdt, NCP_SWIPE_RATE); + if (noff != row_off) { + row_off = noff; + g_ncp_swipe_off = noff; + row_paint = 1; + } + if (noff == row_off_target) { + if (row_off_target == 0) { + /* 回弹复原完成:先把行重画回 0 位(清残留偏移),再清滑动态(无需重排)。 */ + ncp_repaint_row(list_scroll, row_swipe_row, 0); + row_swipe_row = -1; row_settle = 0; row_off = 0; + row_off_target = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + if (anim_ok) ncp_snap_panel(); /* 行已复原:刷新面板快照(供滑出动画用) */ + } else { + /* 滑出删除完成:撤下该通知 + 重采集 + 重排列表(整面板重画)。 */ + if (row_swipe_row >= 0 && row_swipe_row < g_nrows) + meow_notif_dismiss(g_rows[row_swipe_row].id); + row_swipe_row = -1; row_settle = 0; row_off = 0; + row_off_target = 0; row_paint = 0; + g_ncp_swipe_row = -1; g_ncp_swipe_off = 0; + ncp_refresh_rows(); + list_scroll = mnp_scroll_clamp(list_scroll, g_nrows * NCP_ROW_H, + NCP_LIST_BOT - NCP_LIST_TOP); + need_paint = 1; /* 行已移除/重排:整面板(列表区)重画 */ + } + } + } + /* 重绘(面板体区域;状态栏 0-27 由 draw_statusbar 单独维护,不落入此脏区)。 * need_sb=1(打开/主题变化)时 draw_notif_center 内另绘状态栏并标其脏——脏区仍 * 只含「面板体 + 28px 状态栏」两带,绝不整屏重绘。 @@ -7709,6 +7976,14 @@ static void run_notif_center(struct app *apps, int napps, int *want_launch) list_paint = 0; } } + /* 仅重绘该行带(行左滑跟手/回弹/滑出专用:只刷该行矩形,不整面板/不刷状态栏;同受动画门控)。 + * row_swipe_row<0(行已回弹/删除/被取消)则跳过并清标记,避免对已移除行重绘。 */ + if (row_paint) { + last_act = now; + if (row_swipe_row >= 0 && !(anim_ok && anim_p != anim_target)) + ncp_repaint_row(list_scroll, row_swipe_row, row_off); + row_paint = 0; + } /* ~1s 状态栏时钟 */ if (now - last_tick >= 1000) { last_tick = now; diff --git a/patch_v2/meow_launcherd/meow_notif_panel.h b/patch_v2/meow_launcherd/meow_notif_panel.h index 34dbadb..a254b7e 100644 --- a/patch_v2/meow_launcherd/meow_notif_panel.h +++ b/patch_v2/meow_launcherd/meow_notif_panel.h @@ -193,4 +193,55 @@ static inline int mnp_anim_covered_bot(int top, int ph, int off) return top + ph + off; } +/* ---- 勿扰(DND)过滤(通知中心面板内开关 → 基于 urgency 过滤,纯逻辑,可单测)---- + * urgency 见 meow_notif.h:0=LOW 低 / 1=NORMAL 普通 / 2=HIGH 高 / 3=CRITICAL 紧急(缺省 NORMAL)。 + * dnd=0(关):全部显示;dnd=1(开):只显示 urgency>=2(HIGH/CRITICAL)。返回 1=应显示。 */ +static inline int mnp_dnd_should_show(int urgency, int dnd) +{ + if (!dnd) return 1; + return urgency >= 2; +} + +/* ---- 单条通知行左滑删除(纯逻辑,可单测)---- + * 交互(launcher run_notif_center):行内左横滑(横向主导+左移)→ 该行跟随左移(off<0,露出删除底); + * 松手过阈值 → meow_notif_dismiss + 行移除/重排;未过/反向滑回 → 回弹复原(跟手、半程可回)。 + * 本组函数只做标量:clamp / 删除阈值 / 落位步进,不碰 fb/几何——供 launcher 与宿主单测共用。 */ + +/* 行左移 clamp:把手指位移钳到 [-row_w, 0](正向=右移钳 0;越左钳 -row_w=卡全滑出)。 + * row_w=行宽(如 NCP_ROW_W),off<=0 表示左移程度(负得多=滑出多)。 */ +static inline int mnp_swipe_clamp(int off, int row_w) +{ + if (off > 0) off = 0; + if (off < -row_w) off = -row_w; + return off; +} + +/* 松手删除判定:│off│ >= row_w*frac_pct/100 或 │off│ >= min_px → 返回 1(应删除); + * 否则 0(未过阈值/半程取消 → 回弹)。frac_pct 为行宽百分比(如 40)、min_px 为绝对像素保底 + * (防极窄行/极小滑也判删,取二者较大者实际生效——满足「≥row_w 某比例 或 ≥ 阈值 px」)。 */ +static inline int mnp_swipe_delete(int off, int row_w, int frac_pct, int min_px) +{ + if (off > 0) off = 0; + int amt = -off; /* 左移幅度(px,>=0) */ + if (amt >= row_w * frac_pct / 100) return 1; + if (amt >= min_px) return 1; + return 0; +} + +/* 落位动画线性步进:off 沿方向向 target 走 dt_ms*px_per_ms,到 target 即停(可正/反向回弹或滑出)。 + * dt_ms=距上次 tick 单调毫秒差;px_per_ms=速率(px/ms,如 3)。dt_ms<=0 或已到位返回原 off。 + * 单调、可打断(任意时刻改 off 即反向),与面板开关平移动画(mnp_anim_step)同「状态时间轴」思想, + * 但按固定 px/ms 而非剩余比例(行位移远小于面板高,线性更稳)。 */ +static inline int mnp_swipe_step(int off, int target, unsigned long long dt_ms, int px_per_ms) +{ + if (dt_ms == 0 || off == target) return off; + int delta = target - off; + int dir = (delta < 0) ? -1 : 1; + long long absd = (long long)(dir < 0 ? -delta : delta); + long long step = (long long)dt_ms * px_per_ms; + if (step > absd) step = absd; + if (step < 1) step = 1; /* 单调前进,防快循环 dt 极小卡住 */ + return off + dir * (int)step; +} + #endif /* MEOW_NOTIF_PANEL_H */ diff --git a/patch_v2/meow_launcherd/meow_settings.c b/patch_v2/meow_launcherd/meow_settings.c index 7faa076..118742a 100644 --- a/patch_v2/meow_launcherd/meow_settings.c +++ b/patch_v2/meow_launcherd/meow_settings.c @@ -121,6 +121,9 @@ static const meow_setting_def g_defs[] = { { "notify.categories", SET_INT, "4", 1, 4, NULL, NULL }, { "notify.category_quota", SET_INT, "4", 1, 16, NULL, NULL }, { "notify.media_quota", SET_INT, "1", 1, 1, NULL, NULL }, + /* 通知中心「勿扰」面板内开关(SET_BOOL,默认 0=关):开=只显示 urgency>=2(HIGH/CRITICAL)。 + * 面板内 toggle 读写均走 meow_settings_set_bool/get_bool,零旁路(通知中心 UI 方案 §12.2)。 */ + { "notify.dnd", SET_BOOL, "0", 0, 0, NULL, NULL }, /* 日志 */ { "log.level", SET_ENUM, "info", 0, 0, "error,warn,info,debug", "错误,警告,信息,调试" }, { "log.to_card", SET_BOOL, "1", 0, 0, NULL, NULL }, diff --git a/patch_v2/meow_launcherd/test_notif_panel.c b/patch_v2/meow_launcherd/test_notif_panel.c index ed40165..8c45bc1 100644 --- a/patch_v2/meow_launcherd/test_notif_panel.c +++ b/patch_v2/meow_launcherd/test_notif_panel.c @@ -238,6 +238,79 @@ static void test_anim_pure(void) CHECK(mnp_anim_step(0, 0, FIX, 10, 180) == 0, "at p==0 with target 0 stays put (would break)"); } +/* P5 ⑥ 勿扰过滤 + ⑦ 行左滑删除(纯逻辑,header-only): + * - mnp_dnd_should_show:dnd=0 全显示;dnd=1 只 urgency>=2(HIGH/CRITICAL)。 + * - mnp_swipe_clamp:跟手 clamp 到 [-row_w,0]。 + * - mnp_swipe_delete:松手阈值(行宽百分比 / 绝对像素,取大者)。 + * - mnp_swipe_step:落位动画线性步进(回弹/滑出,正反向,到底停,半程取消)。 + * 另加一段 mini 状态机:手指拖 → 松手过阈值=滑出删除 / 未过=回弹复原(跟手、半程可回)。 */ +static void test_dnd_swipe_pure(void) +{ + fprintf(stderr, "--- P5 DND filter + row left-swipe ---\n"); + /* mnp_dnd_should_show */ + CHECK(mnp_dnd_should_show(0, 0) == 1, "dnd off -> LOW(0) shows"); + CHECK(mnp_dnd_should_show(1, 0) == 1, "dnd off -> NORMAL(1) shows"); + CHECK(mnp_dnd_should_show(2, 0) == 1, "dnd off -> HIGH(2) shows"); + CHECK(mnp_dnd_should_show(3, 0) == 1, "dnd off -> CRITICAL(3) shows"); + CHECK(mnp_dnd_should_show(0, 1) == 0, "dnd on -> LOW(0) hidden"); + CHECK(mnp_dnd_should_show(1, 1) == 0, "dnd on -> NORMAL(1) hidden"); + CHECK(mnp_dnd_should_show(2, 1) == 1, "dnd on -> HIGH(2) shows"); + CHECK(mnp_dnd_should_show(3, 1) == 1, "dnd on -> CRITICAL(3) shows"); + + /* mnp_swipe_clamp(跟手 offset clamp:off∈[-row_w,0]) */ + CHECK(mnp_swipe_clamp(-200, 456) == -200, "swipe_clamp in-range kept"); + CHECK(mnp_swipe_clamp(30, 456) == 0, "swipe_clamp rightward -> 0"); + CHECK(mnp_swipe_clamp(-500, 456) == -456, "swipe_clamp past left -> -row_w"); + CHECK(mnp_swipe_clamp(-456, 456) == -456, "swipe_clamp exactly -row_w kept"); + CHECK(mnp_swipe_clamp(0, 456) == 0, "swipe_clamp 0 kept"); + + /* mnp_swipe_delete(row_w=456, frac_pct=40 → 40%=182.4;min_px=110;OR 语义 → 有效阈值=110px) */ + CHECK(mnp_swipe_delete(-80, 456, 40, 110) == 0, "delete -80 (<110 & <182.4) -> cancel"); + CHECK(mnp_swipe_delete(-110, 456, 40, 110) == 1, "delete -110 (>=min_px) -> delete"); + CHECK(mnp_swipe_delete(-150, 456, 40, 110) == 1, "delete -150 (>=min_px, <40%) -> delete"); + CHECK(mnp_swipe_delete(-183, 456, 40, 110) == 1, "delete -183 (>=40%) -> delete"); + CHECK(mnp_swipe_delete(-456, 456, 40, 110) == 1, "delete full slide -> delete"); + CHECK(mnp_swipe_delete(0, 456, 40, 110) == 0, "delete 0 -> cancel"); + /* 窄行:40%=24px(row_w=60),min_px=110 不起作用 → 阈值=24px */ + CHECK(mnp_swipe_delete(-25, 60, 40, 110) == 1, "delete narrow row -25 (>=40%) -> delete"); + CHECK(mnp_swipe_delete(-20, 60, 40, 110) == 0, "delete narrow row -20 (<40%) -> cancel"); + + /* mnp_swipe_step(线性步进:off 向 target 走 dt*rate,可正/反向,到底停) */ + CHECK(mnp_swipe_step(-100, 0, 10, 3) == -70, "step springback +30px/tick"); + CHECK(mnp_swipe_step(-100, 0, 0, 3) == -100, "step dt=0 -> unchanged"); + CHECK(mnp_swipe_step(-100, 0, 1000, 3) == 0, "step huge dt -> clamp at 0 (springback done)"); + CHECK(mnp_swipe_step(-50, -456, 10, 3) == -80, "step slide-out leftward"); + CHECK(mnp_swipe_step(-50, -456, 1000, 3) == -456, "step huge dt -> clamp at -row_w (deleted)"); + CHECK(mnp_swipe_step(-30, -30, 10, 3) == -30, "step at target -> stays"); + CHECK(mnp_swipe_step(0, 0, 10, 3) == 0, "step 0 target 0 -> stays"); + CHECK(mnp_swipe_step(-1000, 0, 5, 3) == -985, "step linear rate not exponential (small dt)"); + + /* mini 状态机:跟手拖 → 松手判定 → 动画到目标(半程取消 vs 删除)。 */ + int row_w = 456, off = 0; + /* 拖 80px 左(低于阈值 110)→ 松手回弹复原 */ + off = mnp_swipe_clamp(-80, row_w); + CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: drag -80 -> cancel (springback)"); + int target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0; + off = mnp_swipe_step(off, target, 1000, 3); + CHECK(off == 0, "mini: springback reaches 0 (row restored)"); + /* 拖 150px 左(>=110)→ 松手滑出删除 */ + off = mnp_swipe_clamp(-150, row_w); + CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: drag -150 -> delete"); + target = mnp_swipe_delete(off, row_w, 40, 110) ? -row_w : 0; + off = mnp_swipe_step(off, target, 1000, 3); + CHECK(off == -row_w, "mini: slide-out reaches -row_w (dismiss)"); + /* 半程可反向:拖 200 左再反向回摄(dx 变 +30)→ off 回 0(跟手可回、松手在途取消) */ + off = mnp_swipe_clamp(-200, row_w); + CHECK(off == -200, "mini: drag -200 follows finger"); + off = mnp_swipe_clamp(30, row_w); /* 反向拖回原点左侧以右 → clamp 0 */ + CHECK(off == 0, "mini: reverse drag -> row slides back to 0 (halfway cancel)"); + /* 半程取消:拖 150 左(未达 182.4 的 40% 但达 110px 阈值)→ 松手删;若再拖回 <110 → 回弹 */ + off = mnp_swipe_clamp(-150, row_w); + CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 1, "mini: -150 passed threshold -> delete"); + off = mnp_swipe_clamp(-100, row_w); /* 又拖回 100 <110 */ + CHECK(mnp_swipe_delete(off, row_w, 40, 110) == 0, "mini: -100 back under threshold -> cancel"); +} + static void test_storage_contract(void) { fprintf(stderr, "--- P0 storage contract (panel reads) ---\n"); @@ -341,6 +414,7 @@ int main(void) test_pure_logic(); test_scroll_pure(); test_anim_pure(); + test_dnd_swipe_pure(); test_storage_contract(); unlink(g_tflog); rmdir(dir);