diff --git a/patch_v2/files_app/files b/patch_v2/files_app/files index a88853f..fed28a8 100755 Binary files a/patch_v2/files_app/files and b/patch_v2/files_app/files differ diff --git a/patch_v2/hello_app/hello b/patch_v2/hello_app/hello index c6b335f..79c6281 100755 Binary files a/patch_v2/hello_app/hello and b/patch_v2/hello_app/hello differ diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c index f420acb..1636111 100644 --- a/patch_v2/meow_launcherd/main_menu.c +++ b/patch_v2/meow_launcherd/main_menu.c @@ -56,17 +56,28 @@ static int menu_live_open_pending = 0; * fresh menu taps. A replayed tap on an app row relaunches the app (symptom: * BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO * PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen - * flashes back to the player — Bug 4). We drain the queue immediately after - * the app exits and ignore touches for a short cooldown that covers the - * measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */ + * flashes back to the player — Bug 4). + * + * M39:真正的机制是「排空本进程 touch_fd」——evdev 每个 fd 各收一份事件副本,App 运行期间 + * (launcher 不读)的全部事件都堆在这个 fd 里,app 一退出就会作为「新菜单 tap」重放。 + * menu_drain_touch() 把它们全读掉(含按下沿)+ 复位手势状态机,历史上用于兜底的 + * 1.2–2.25s 冷却窗口因此可以收窄到 MENU_TOUCH_GUARD_MS:那个窗口是从「App 退出」开始计时 + * 的,用户看到菜单后伸手的第一下必然落在里面被吞(用户实测「首页点图标要点两次」根因之一)。 + * 现在只留极短的驱动尾巴守卫,不再按时间吞真实输入。 */ static unsigned long long menu_touch_cooldown_until = 0; +/* 排空本进程触摸队列(非阻塞读到 EAGAIN)+ 复位手势状态机。 + * 队列/down 边沿都要清:只清队列而保留 t_down,会让「按下已被 drain 掉、抬手稍后才到」的 + * 这次手势被当成一次有效 tap(ghost touch 重放的另一半)。 */ static void menu_drain_touch(void) { if (touch_fd >= 0) { unsigned char ev[16]; while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ } } + t_down = 0; t_down_valid = 0; + t_down_x = t_down_y = -1; + t_last_x = t_last_y = -1; } static void menu_arm_touch(long ms) @@ -76,18 +87,19 @@ static void menu_arm_touch(long ms) } -/* Drain the touch queue + arm the post-app cooldown (call right after the - * app exits, before the menu resumes polling touch). The drain always runs so - * touches queued by the launch-attempt tail are cleared; the cooldown only - * arms when an app actually ran (app_ran) so a failed launch doesn't lock the - * menu out for 2.5s. */ +/* Drain the touch queue + arm the post-app guard (call right after the app + * exits, before the menu resumes polling touch). The drain always runs so + * touches queued by the launch-attempt tail are cleared; the guard only arms + * when an app actually ran (app_ran) so a failed launch doesn't lock the menu + * out. */ static void menu_after_app_exit(int app_ran) { menu_drain_touch(); - /* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。 - * 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短, - * 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */ - if (app_ran) menu_touch_cooldown_until = now_ms() + 1200; + /* M39:M2.6 的 2500ms → M2.22 的 1200ms → 现在的 MENU_TOUCH_GUARD_MS(150ms)。 + * 依据:重放的来源是本进程 fd 里 App 运行期的事件,menu_drain_touch() 已一次性清空 + * (含 t_down/原点,见上),窗口只兜「面板重配后驱动补发的幽灵尾巴」。1200ms 会稳定吞掉 + * 用户「返回桌面后立即点下一个图标」——正是要修的症状。 */ + if (app_ran) menu_touch_cooldown_until = now_ms() + MENU_TOUCH_GUARD_MS; } @@ -1207,8 +1219,9 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, /* touch: event reads (event1 is not grabbed) */ touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK); if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno)); - /* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */ - menu_arm_touch(TAP_ARM_DELAY_MS); + /* M39:菜单激活(启动)即 drain + 极短守卫(M2.6 原为 300ms;界面此时已整帧画完, + * 窗口只会吞用户看到首页后的第一下,故收窄到 MENU_TOUCH_GUARD_MS) */ + menu_arm_touch(MENU_TOUCH_GUARD_MS); /* key polling */ struct keypoll kp_power, kp_keys; @@ -1335,7 +1348,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, sim_exit_tap_done = 1; if (meow_mode) { int rb = confirm_reboot_player(); - menu_arm_touch(TAP_ARM_DELAY_MS); + menu_arm_touch(MENU_TOUCH_GUARD_MS); dirty = 1; logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb); } else { @@ -1482,7 +1495,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0; pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */ dirty = 1; /* 取消后重绘应用列表 */ - menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */ + menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原 M2.6 800ms) */ /* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。 * 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」, * 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at, @@ -1666,7 +1679,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, 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); + menu_arm_touch(MENU_TOUCH_GUARD_MS); notif_click_t click; memset(&click, 0, sizeof click); click.app_idx = -1; @@ -1682,7 +1695,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); - menu_arm_touch(800); + menu_arm_touch(MENU_TOUCH_GUARD_MS); dirty = 1; /* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */ if (click.status == NOTIF_CLICK_STATUS_OK && @@ -1764,7 +1777,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, * 本函数只在顶部热区(down_y notification center (release edge, fallback)"); - menu_arm_touch(TAP_ARM_DELAY_MS); + menu_arm_touch(MENU_TOUCH_GUARD_MS); notif_click_t click; memset(&click, 0, sizeof click); click.app_idx = -1; @@ -1780,7 +1793,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); - menu_arm_touch(800); + menu_arm_touch(MENU_TOUCH_GUARD_MS); dirty = 1; /* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */ if (click.status == NOTIF_CLICK_STATUS_OK && @@ -1818,7 +1831,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, if (dx <= -MENU_SWIPE_THRESH && adx > ady && tx >= MEOW_EDGE_W && ty >= SB_BAR_H) { logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy); - menu_arm_touch(TAP_ARM_DELAY_MS); + menu_arm_touch(MENU_TOUCH_GUARD_MS); run_now_playing(apps, napps); /* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */ /* 重播音量基线(key_router)+ 本页播放/电源基线 */ @@ -1830,7 +1843,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); - menu_arm_touch(800); + menu_arm_touch(MENU_TOUCH_GUARD_MS); dirty = 1; continue; } @@ -1840,7 +1853,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, /* 批③:顶栏居右「后台管理」按钮(最高优先级;阻挡非阻塞,返回后重画) */ if (menu_hit_bgm_btn(tx, ty)) { logmsg("menu: touch topbar bg-manager button"); - menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ + menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */ run_bg_page(apps, napps); /* 重播音量基线(key_router)+ 本页播放/电源基线 */ key_router_rebase(); @@ -1851,7 +1864,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); - menu_arm_touch(800); + menu_arm_touch(MENU_TOUCH_GUARD_MS); dirty = 1; continue; } @@ -1863,7 +1876,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, * 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */ logmsg("meow mode: BACK TO PLAYER -> confirm reboot"); int rb = confirm_reboot_player(); - menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */ + menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */ dirty = 1; if (rb) { logmsg("reboot-to-player: yes, rebooting"); } /* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */ @@ -1878,7 +1891,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, * 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */ else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) { logmsg("menu: touch settings row -> settings page"); - menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */ + menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */ run_settings_page(); /* 重播音量基线(key_router)+ 本页播放/电源基线 */ key_router_rebase(); @@ -1889,7 +1902,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, m_prev_pwr = kp_is_down(&kp_power, KEY_POWER); m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE); last_input_ms = now_ms(); /* 回主界面即视为输入 */ - menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */ + menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原设置页返回 800ms) */ dirty = 1; /* 全帧重画应用列表 */ } /* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与 diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index 8c28eac..7240f80 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -178,7 +178,12 @@ static inline const char *meow_str_zh(const char *id, const char *zh) #define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */ #define NCP_DRAG_ENTER_SLOP 16 #define NCP_DRAG_THR_PCT 40 -#define TAP_ARM_DELAY_MS 300 +/* M39:launcher 触摸守卫窗口(取代 M2.6「菜单关闭后忽略触摸」800ms / M2.22 的 App 退出后 + * 1200ms / M2.6 打开子页 300ms)。语义只剩「drain 队列后仍防面板重配尾巴的幽灵按下」, + * 不再吞用户看到界面后的第一下:窗口起点固定在 drain 之后,且只覆盖极短的驱动尾巴。 + * 依据:每次 arm 前都先 menu_drain_touch() 清本进程 touch_fd 队列(evdev 每 fd 各收一份, + * App/子页运行期间的全量事件都在本 fd 里,drain 后已无历史事件可重放)。 */ +#define MENU_TOUCH_GUARD_MS 150 #define PM_NUM 6 #define PM_R 50 /* 圆钮半径(直径 100) */ #define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */ diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index c799a19..82eb9bf 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/ncm_app/ncm b/patch_v2/ncm_app/ncm index 4999543..ef51ff8 100755 Binary files a/patch_v2/ncm_app/ncm and b/patch_v2/ncm_app/ncm differ diff --git a/patch_v2/sdk/meow_ui_input.c b/patch_v2/sdk/meow_ui_input.c index 514aa79..f04e70e 100644 --- a/patch_v2/sdk/meow_ui_input.c +++ b/patch_v2/sdk/meow_ui_input.c @@ -117,10 +117,23 @@ static volatile int g_exit = 0; static meow_ui_events g_events; static int g_events_set = 0; static uint64_t last_tick_ms = 0; -/* M2.22(C):启动 cooldown。应用刚进入时,用户点列表那几下(BTN_TOUCH 残留/抬手尾巴) - * 可能被当作「左缘滑动」→ 立即返回。窗口期丢弃 SWIPE(尤其 LEFT),仍接受 TAP。 */ -static uint64_t startup_cooldown_until = 0; -#define STARTUP_COOLDOWN_MS 500 +/* M39:启动输入门 = 一次性排空 + 极短「系统手势」防穿透(取代 M2.22(C) 的 500ms 全量吞窗)。 + * + * 旧实现从 meow_ui_run 入口起 500ms 内丢弃 TAP/SWIPE/HOME/NOTIF_PULL/TOUCH_DOWN/MOVE/LONG_PRESS。 + * 窗口起点在 App 画完首帧之后(reader/files 都是先 page_push 再 run),用户看到界面后伸手的 + * 第一下必然落在窗口里被吞 → launcher/reader「要点两次」、files 概率、ncm/picbrowser 因首屏 + * 更晚(LOADING/缩略图)点得晚才稳定成功。 + * + * 残留输入的真实来源只有一处:evdev fd 在 input 线程启动时才 open,App 之前根本收不到; + * 能进队列的只有「fd 打开 → 进入分发」之间极短的尾巴 —— meow_input_drain() 一次性清掉即可 + * (清环形队列 + 复位触摸状态机,见 meow_input_drain)。故: + * - TAP / TOUCH_DOWN / TOUCH_MOVE / LONG_PRESS 不再按时间吞(用户第一下立即生效); + * - 系统手势(左缘返回 SWIPE、底部上滑回桌面 HOME、顶部下拉通知中心、滑动进度条)保留 + * STARTUP_GESTURE_GUARD_MS 防穿透:启动瞬间的幽灵手势不得把刚进入的 App 直接弹回桌面。 + * 跨启动的在途手势仍不会产生 TAP:fd 打开前发生的按下 App 看不到(t_down=0),其抬手被 + * touch_release 的首行 !t_down 早退丢弃。 */ +static uint64_t startup_gesture_guard_until = 0; +#define STARTUP_GESTURE_GUARD_MS 150 /* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */ static const char *meow_settings_path(void) @@ -708,19 +721,23 @@ void meow_ui_run(const meow_ui_events *ev) if (ev) { g_events = *ev; g_events_set = 1; } g_exit = 0; meow_input_start(); + /* M39:启动排空一次(清掉 evdev fd 打开到进入分发之间堆积的事件尾巴 + 复位触摸状态机), + * 取代 M2.22(C) 的 500ms 按时间吞输入窗口。窗口只保留给系统手势(防穿透),见下。 */ + meow_input_drain(); last_tick_ms = meow_ui_time_ms(); - startup_cooldown_until = meow_ui_time_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */ + startup_gesture_guard_until = meow_ui_time_ms() + STARTUP_GESTURE_GUARD_MS; while (!g_exit) { /* agent-119:实时触摸按压/按住/释放 → 即时重绘命中按钮区(不等 app 重绘)。 * IME 打开时软键盘为覆盖层且拦截触摸,不驱动背后按钮视觉,跳过。 - * §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动;启动窗口不触发)。 */ + * §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动)。 + * M39:长按不再受启动窗口门控(长按需要按住 ≥ 阈值,启动残留手势已由 drain 清掉)。 */ if (!meow_ime_active()) { meow_widget_touch_refresh(); - if (meow_ui_time_ms() >= startup_cooldown_until) meow_input_long_press_poll(); + meow_input_long_press_poll(); } else if (meow_ime_mode_pinyin()) { /* 拼音 IME 打开:喂长按轮询(长按退格清空字段)。英文模式不喂(保持既有行为)。 */ - if (meow_ui_time_ms() >= startup_cooldown_until) meow_input_long_press_poll(); + meow_input_long_press_poll(); } meow_input_ev e; @@ -729,10 +746,10 @@ void meow_ui_run(const meow_ui_events *ev) if (g_exit) break; /* 已请求退出:不再分发剩余事件,杜绝「退出后又触发」的偶发再进/退 */ got = 1; meow_ui_activity_ping(); /* M2.71-bug5:任意交互事件都喂 launcher 重置息屏计时(节流) */ - int in_startup = (meow_ui_time_ms() < startup_cooldown_until); /* M2.22(C) */ + int in_gesture_guard = (meow_ui_time_ms() < startup_gesture_guard_until); /* M39 */ if (e.type == MEOW_EV_TAP) { - if (in_startup) continue; /* 启动窗口也吞 TAP:旧手势残留点按(如上一次左滑抬起) - * 在进入瞬间命中 UI 按钮(如全屏按钮)会误触发——丢弃。 */ + /* M39:TAP 不再按时间吞——用户看到界面后的第一下立即生效(旧 500ms 窗口在 + * 首帧之后 arm,正是「要点两次」的根因;残留手势已在 run 入口 drain)。 */ /* 抬手未达标:清视觉条 */ meow_ui_swipe_hint(-1); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); @@ -754,7 +771,8 @@ void meow_ui_run(const meow_ui_events *ev) if (meow_view_dispatch_event(MEOW_EV_KEY, e.a, e.b)) continue; if (g_events_set && g_events.on_key) g_events.on_key(e.a, e.b); } else if (e.type == MEOW_EV_SWIPE) { - if (in_startup) continue; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */ + if (in_gesture_guard) continue; /* M39:仅系统手势防穿透(启动瞬间幽灵手势不得把 + * 刚进入的 App 直接弹回桌面);TAP 已不在此门控。 */ /* 抬手达标:清视觉条(左滑返回),触发行消失 */ meow_ui_swipe_hint(-1); meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6); @@ -784,7 +802,7 @@ void meow_ui_run(const meow_ui_events *ev) * 批① meow_bg_add 保活,进程保留转后台、SDK present 自抑);**不**调 * meow_ui_request_exit()——那会断送保活(进程 exit(0))。launcher 据 app 声明 * (background_keepalive/background_audio+媒体活跃)决定无限期/宽限。 */ - if (in_startup) continue; /* 启动窗口误触残留:丢弃 */ + if (in_gesture_guard) continue; /* M39:系统手势防穿透(与 SWIPE 同窗口) */ meow_ui_swipe_hint(-1); meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */ /* IME 打开时软键盘覆盖底部:键盘拥有底部区,把事件交给 IME 且**绝不**回桌面 @@ -792,7 +810,7 @@ void meow_ui_run(const meow_ui_events *ev) if (meow_ime_active()) { (void)meow_ime_handle_event(&e); continue; } meow_ui_nav_request_exit(); } else if (e.type == MEOW_EV_NOTIF_PULL) { - if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */ + if (in_gesture_guard) continue; /* M39:系统手势防穿透(防进入即开通知中心) */ /* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发 * APP on_swipe;路由到系统钩子(App 进程只检测+经 IPC 上报 MSG_NOTIF_OPEN, * 通知中心由 launcher 渲染,见 FRAMEWORK_DESIGN §10.5)。 @@ -803,10 +821,11 @@ void meow_ui_run(const meow_ui_events *ev) meow_ui_sys_notif_pull(e.a); /* e.a = 手势按下原点 Y */ } else if (e.type == MEOW_EV_TOUCH_DOWN || e.type == MEOW_EV_TOUCH_MOVE) { /* M2.71-bug2/6:拖动过程(跟手)。IME 弹出时触摸优先喂键盘(键盘覆盖层拦截, - * on_touch_* 丢弃,避免按到键盘却在背后控件上锁拖拽);启动窗口也吞(防残留点按 - * 误锁拖拽)。模态(dialog/sheet)期间本主循环被阻塞,天然不在此分发。 + * on_touch_* 丢弃,避免按到键盘却在背后控件上锁拖拽)。模态(dialog/sheet)期间 + * 本主循环被阻塞,天然不在此分发。 + * M39:不再受启动窗口门控——跟手拖动是用户可感知的第一下交互;启动残留点按 + * 已由 run 入口 meow_input_drain()(含触摸状态机复位)清掉,不会锁错拖拽。 * IME 打开时先把压点喂给拼音(候选栏滑动翻页判定 g_ime_swipe_y),再丢弃。 */ - if (in_startup) continue; if (meow_ime_active()) { meow_ime_handle_event(&e); continue; } /* §6.4:down 捕获——命中目标捕获整个手势直到 up;随后 move/up 只发给该目标 * (防滑出后误触发下层)。真实触发放置捕获(宿主直调 on_touch_* 不受影响)。 */ @@ -817,7 +836,7 @@ void meow_ui_run(const meow_ui_events *ev) g_events.on_touch_move(e.a, e.b); got = 1; /* 交互事件:不睡眠,保持跟手帧同步 */ } else if (e.type == MEOW_EV_SWIPE_PROGRESS) { - if (in_startup) continue; /* 启动窗口进度条静音 */ + if (in_gesture_guard) continue; /* M39:系统手势防穿透(启动进度条静音) */ int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */ /* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。 * 全屏/收合下「左缘返回手势保留」,顶部动态提示条(meow_ui_swipe_hint)照常绘制 @@ -833,8 +852,8 @@ void meow_ui_run(const meow_ui_events *ev) } else if (e.type == MEOW_EV_LONG_PRESS) { /* §6.3 通用长按:按下 ≥ 阈值未抬手触发。坐标=触发时刻按下点(App 用 * meow_widget_hit/meow_listview_hit 判定命中控件/行)。已注册回调则分发; - * 其抬手已被 touch_release 抑制(不再产生 TAP/SWIPE),此处无需吞后续 tap。 */ - if (in_startup) continue; + * 其抬手已被 touch_release 抑制(不再产生 TAP/SWIPE),此处无需吞后续 tap。 + * M39:不再受启动窗口门控(长按需按住达阈值,残留手势已由 drain 清掉)。 */ /* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */ if (meow_ime_active() && meow_ime_handle_event(&e)) continue; meow_ui_gesture_end(); /* §6.4:长按已触发,其抬手不再 TAP/SWIPE——顺手释放捕获 */ diff --git a/patch_v2/sdk/test_ime_fix.c b/patch_v2/sdk/test_ime_fix.c index 5fd4cad..353a9de 100644 --- a/patch_v2/sdk/test_ime_fix.c +++ b/patch_v2/sdk/test_ime_fix.c @@ -166,7 +166,7 @@ static void host_tap(int x, int y) static void *driver(void *arg) { (void)arg; - usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */ + usleep(600000); /* 等 meow_ui_run 起动(M39: 启动守卫 150ms) */ meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */ usleep(350000); /* 等过 IME 打开 TAP 冷却窗(200ms,bug3)+ drain 40ms */ meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */ diff --git a/patch_v2/sdk/test_meow_ui_dispatch.c b/patch_v2/sdk/test_meow_ui_dispatch.c index 685327c..d683442 100644 --- a/patch_v2/sdk/test_meow_ui_dispatch.c +++ b/patch_v2/sdk/test_meow_ui_dispatch.c @@ -8,7 +8,9 @@ * M3 捕获手势滑出不误触:gesture_begin 命中按钮 A 后捕获;任凭概念滑动到远处(不重新命中), * meow_ui_gesture_owner 恒为 A(绝不滑出误触下层/邻控件);end 释放; * M4 list 焦点点外释放:meow_list_tap_at/meow_listview_tap_at 使列表成为唯一焦点; - * focus_tap 落在列表内 → 保位;落在列表外(页面内容/空白)→ 释放焦点并触发 lost 回调。 + * focus_tap 落在列表内 → 保位;落在列表外(页面内容/空白)→ 释放焦点并触发 lost 回调; + * M5 启动排空 drain 语义(M39):drain 清空已入队事件 + 复位在途手势(迟到抬手不产 TAP), + * 且 drain 之后的新按下仍正常产出 TAP(排空只清残留,不吞新输入)。 * * 编译(宿主 gcc,链真实 widget/draw/input/activity/gfx/sys/theme/vol/log/dialog/ime/notify/ipc + * 打桩 font/global;完整超集便于链接): @@ -23,10 +25,15 @@ #include "meow_ui.h" #include "meow_ui_internal.h" #include "meow_ui_view.h" +#include "meow_ui_nav.h" /* meow_ui_nav_request_exit:meow_ui_input.c 的 MEOW_EV_HOME 分支引用 */ #include #include #include +/* 不链 meow_ui_nav.c:以桩满足 HOME 分支的链接依赖(同 test_meow_ui_home_gesture 范式)。 */ +static int g_nav_exit_calls = 0; +void meow_ui_nav_request_exit(void) { g_nav_exit_calls++; } + static int g_fail = 0; #define CHECK(cond, name) do { \ if (cond) { printf(" PASS %s\n", name); } \ @@ -132,6 +139,35 @@ int main(void) CHECK(g_lost_calls == 2, "lost callback fired again"); (void)btnB; + /* ================= M5:启动排空 drain 语义(M39) ================= + * M39 用「meow_ui_run 入口 drain 一次」取代 M2.22(C) 的 500ms 按时间吞输入窗口, + * 所以 drain 必须真的清干净:①队列里已入队的事件被丢弃;②在途手势(按下已发生、 + * 抬手尚未到)被复位 —— 抬手不得再产出 TAP(否则 drain 之后会冒出迟到点击); + * ③drain 之后的新按下仍能正常产出 TAP(排空只清残留,不吞新输入)。 */ + printf("M5 startup drain semantics (queue + in-flight gesture)\n"); + { + /* ① 队列排空 */ + meow_input_inject(MEOW_EV_TAP, 10, 20); + meow_input_inject(MEOW_EV_TAP, 30, 40); + meow_input_drain(); + meow_input_ev e; + CHECK(!meow_input_pop(&e), "drain empties already-queued events"); + + /* ② 在途手势:按下 → drain → 抬手,不得产生迟到 TAP */ + meow_input_touch_set(1, 120, 130, 0); + meow_input_drain(); + meow_input_touch_release(); + CHECK(!meow_input_pop(&e), "drain resets in-flight gesture (late release -> no TAP)"); + + /* ③ drain 之后的新一次点击正常产出 */ + meow_input_touch_set(1, 200, 300, 0); + meow_input_touch_release(); + int ok = meow_input_pop(&e); + int coords_ok = ok && e.type == MEOW_EV_TAP && e.a == 200 && e.b == 300; + if (!coords_ok && ok) printf(" tap after drain: type=%d a=%d b=%d\n", e.type, e.a, e.b); + CHECK(coords_ok, "tap after drain still dispatches"); + } + /* 清理:控件释放自动注销 z 序绘制物 + 焦点/捕获复位 */ meow_widget_free(list); meow_widget_free(btnB); diff --git a/patch_v2/sdk/test_meow_ui_home_gesture.c b/patch_v2/sdk/test_meow_ui_home_gesture.c index dd3be89..72e1087 100644 --- a/patch_v2/sdk/test_meow_ui_home_gesture.c +++ b/patch_v2/sdk/test_meow_ui_home_gesture.c @@ -12,6 +12,14 @@ * 3b 未注册 on_swipe:注入 MEOW_EV_SWIPE(UP)(中部上滑)→ nav_request_exit 不触发(App 滚动); * 3c 注册 on_swipe:注入 MEOW_EV_HOME → nav_request_exit 触发(系统级手势优先于 App on_swipe), * on_swipe 不被调用——屏底热区上滑是系统保留手势,任意 App 内生效(用户 2026-09-04 批②)。 + * T4 启动输入门(M39,取代 M2.22(C) 的 500ms 全量吞窗:启动 drain + ≤150ms 系统手势防穿透): + * 4a 启动后立即 TAP → on_tap 立即生效(旧窗口会吞,就是「要点两次」的根因); + * 4b 启动后立即 HOME → 守卫期不触发(防穿透);守卫过后再上滑 → 触发一次; + * 4c 启动后立即 SWIPE(LEFT) → 守卫期不下发;守卫过后 → 下发一次; + * 4d 启动后立即 TOUCH_DOWN / TOUCH_MOVE → 跟手回调立即收到; + * 4e 启动后立即 LONG_PRESS → 长按回调立即收到; + * 4f meow_ui_run 入口 drain → run 之前已入队的 TAP/HOME/SWIPE 全部丢弃(旧时间窗要解决的 + * 「残留输入」改由一次性排空解决,且不影响 4a 的新输入)。 * * 编译(宿主 gcc,链真实 SDK 源全集 + 打桩 font/global;**不链 meow_ui_nav.c**——本测以测试内 * meow_ui_nav_request_exit 桩计数验证触发): @@ -140,7 +148,7 @@ static int g_inj_type, g_inj_a, g_inj_b; static void *driver(void *arg) { (void)arg; - usleep(700000); /* 等 startup cooldown(500ms) 过后再注入 */ + usleep(700000); /* 等启动守卫(M39: 150ms,原 500ms cooldown)过后再注入 */ meow_input_inject(g_inj_type, g_inj_a, g_inj_b); usleep(80000); meow_ui_request_exit(); @@ -181,6 +189,128 @@ static void test_dispatch(void) CHECK(g_swipe_calls == 0, "T3c system-priority home -> on_swipe NOT called"); } +/* ============ T4 启动输入门(M39) ============ + * 语义:meow_ui_run 入口 meow_input_drain() 排空一次(清队列 + 复位触摸状态机),随后只对 + * 「系统手势」(SWIPE / HOME / NOTIF_PULL / SWIPE_PROGRESS)保留 ≤150ms 防穿透; + * TAP / TOUCH_DOWN / TOUCH_MOVE / LONG_PRESS 立即生效。 */ +#define T4_GUARD_WAIT_MS 260 /* > STARTUP_GESTURE_GUARD_MS(150),留调度余量 */ + +static volatile int g_ticks = 0; /* run 循环 tick 计数(注入时序锚点) */ +static void tick_marker(void) { g_ticks++; } + +static int g_taps = 0, g_tap_x = -1, g_tap_y = -1; +static void tap_marker(int x, int y) { g_taps++; g_tap_x = x; g_tap_y = y; } +static int g_downs = 0, g_moves = 0; +static void down_marker(int x, int y) { (void)x; (void)y; g_downs++; } +static void move_marker(int x, int y) { (void)x; (void)y; g_moves++; } +static int g_longs = 0; +static void long_marker(int x, int y) { (void)x; (void)y; g_longs++; } + +static int g_inj2_type, g_inj2_a, g_inj2_b, g_inj_later; + +/* 首个 tick(run 循环已进入、run 入口 drain 已过)注入一次;g_inj_later=1 时越过守卫再注入一次。 */ +static void *drv_startup(void *arg) +{ + (void)arg; + while (g_ticks < 1) usleep(1000); + meow_input_inject(g_inj_type, g_inj_a, g_inj_b); + if (g_inj_later) { + usleep(T4_GUARD_WAIT_MS * 1000); + meow_input_inject(g_inj2_type, g_inj2_a, g_inj2_b); + } + usleep(80000); + meow_ui_request_exit(); + return NULL; +} + +static void run_startup_scenario(int type, int a, int b, int later, + void (*tap_cb)(int, int), void (*swipe_cb)(int, int, int), + void (*td_cb)(int, int), void (*tm_cb)(int, int)) +{ + g_inj_type = type; g_inj_a = a; g_inj_b = b; + g_inj2_type = type; g_inj2_a = a; g_inj2_b = b; + g_inj_later = later; + g_ticks = 0; g_taps = 0; g_tap_x = g_tap_y = -1; + g_downs = 0; g_moves = 0; g_longs = 0; + g_nav_exit_calls = 0; + g_swipe_calls = 0; g_swipe_dir = -1; g_swipe_dx = 0; + meow_input_drain(); + + meow_ui_events ev; memset(&ev, 0, sizeof ev); + ev.on_tick = tick_marker; + ev.on_tap = tap_cb; + ev.on_swipe = swipe_cb; + ev.on_touch_down = td_cb; + ev.on_touch_move = tm_cb; + + pthread_t thd; pthread_create(&thd, NULL, drv_startup, NULL); + meow_ui_run(&ev); + pthread_join(thd, NULL); +} + +/* run 之前已入队(模拟「进入 App 前就堆积的残留输入」),run 入口 drain 必须全部丢掉。 */ +static void *drv_predrain(void *arg) +{ + (void)arg; + usleep(T4_GUARD_WAIT_MS * 1000); /* 越过守卫:证明不是被窗口吞,而是被 drain 清掉 */ + meow_ui_request_exit(); + return NULL; +} + +static void run_predrain_scenario(void) +{ + g_taps = 0; g_nav_exit_calls = 0; g_swipe_calls = 0; + meow_input_drain(); + meow_input_inject(MEOW_EV_TAP, 120, 200); + meow_input_inject(MEOW_EV_HOME, MEOW_SWIPE_UP, 120); + meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80); + + meow_ui_events ev; memset(&ev, 0, sizeof ev); + ev.on_tap = tap_marker; + ev.on_swipe = app_on_swipe; + + pthread_t thd; pthread_create(&thd, NULL, drv_predrain, NULL); + meow_ui_run(&ev); + pthread_join(thd, NULL); +} + +static void test_startup_gate(void) +{ + printf("T4 startup input gate (drain + <=150ms system-gesture guard)\n"); + + /* 4a 启动后立即点击:必须生效(旧 500ms 窗口在首帧后 arm → 会吞掉这一下) */ + run_startup_scenario(MEOW_EV_TAP, 120, 200, 0, tap_marker, NULL, NULL, NULL); + CHECK(g_taps == 1, "T4a immediate TAP after start -> on_tap fired (got %d)", g_taps); + CHECK(g_taps == 1 && g_tap_x == 120 && g_tap_y == 200, "T4a tap coords preserved (%d,%d)", g_tap_x, g_tap_y); + + /* 4b 启动后立即上滑回桌面:守卫期不触发;守卫过后同一手势触发一次 */ + run_startup_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 1, NULL, NULL, NULL, NULL); + CHECK(g_nav_exit_calls == 1, "T4b immediate HOME blocked by guard, post-guard HOME fires exactly once (got %d)", g_nav_exit_calls); + + /* 4c 启动后立即左缘返回 SWIPE:守卫期不下发;守卫过后下发一次(行为与修改前一致) */ + run_startup_scenario(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80, 1, NULL, app_on_swipe, NULL, NULL); + CHECK(g_swipe_calls == 1, "T4c immediate SWIPE(LEFT) blocked by guard, post-guard delivers exactly once (got %d)", g_swipe_calls); + CHECK(g_swipe_dir == MEOW_SWIPE_LEFT, "T4c swipe dir == LEFT (got %d)", g_swipe_dir); + + /* 4d 启动后立即跟手拖动:DOWN/MOVE 立即下发(不再被启动窗口吞) */ + run_startup_scenario(MEOW_EV_TOUCH_DOWN, 240, 300, 0, NULL, NULL, down_marker, move_marker); + CHECK(g_downs == 1, "T4d immediate TOUCH_DOWN delivered (got %d)", g_downs); + run_startup_scenario(MEOW_EV_TOUCH_MOVE, 240, 350, 0, NULL, NULL, down_marker, move_marker); + CHECK(g_moves == 1, "T4d immediate TOUCH_MOVE delivered (got %d)", g_moves); + + /* 4e 启动后立即长按:长按回调立即收到(长按本身需按住达阈值,启动窗口门控已移除) */ + meow_ui_set_long_press_cb(long_marker); + run_startup_scenario(MEOW_EV_LONG_PRESS, 200, 420, 0, NULL, NULL, NULL, NULL); + CHECK(g_longs == 1, "T4e immediate LONG_PRESS delivered (got %d)", g_longs); + meow_ui_set_long_press_cb(NULL); + + /* 4f run 入口 drain:进入前堆积的 TAP/HOME/SWIPE 全部丢弃(替代旧时间窗的防残留语义) */ + run_predrain_scenario(); + CHECK(g_taps == 0, "T4f pre-queued TAP drained at run entry (got %d)", g_taps); + CHECK(g_nav_exit_calls == 0, "T4f pre-queued HOME drained at run entry (got %d)", g_nav_exit_calls); + CHECK(g_swipe_calls == 0, "T4f pre-queued SWIPE drained at run entry (got %d)", g_swipe_calls); +} + int main(void) { meow_ui_opts o = { FBW, FBH }; @@ -189,6 +319,7 @@ int main(void) test_predicate(); test_touch_classify(); test_dispatch(); + test_startup_gate(); meow_ui_shutdown(); printf("home_gesture: %d pass / %d fail\n", g_pass, g_fail); diff --git a/patch_v2/sdk/test_meow_ui_view_event.c b/patch_v2/sdk/test_meow_ui_view_event.c index 1517cb5..a0951d8 100644 --- a/patch_v2/sdk/test_meow_ui_view_event.c +++ b/patch_v2/sdk/test_meow_ui_view_event.c @@ -61,7 +61,7 @@ static int vev_child(meow_view *v, int ev, int a, int b, void *ctx) static void *driver(void *arg) { (void)arg; - usleep(700000); /* 等 startup cooldown(500ms) 过后再发 */ + usleep(700000); /* 等启动守卫(M39: 150ms)过后再发 */ meow_input_inject(MEOW_EV_KEY, MEOW_KEY_VOLUMEUP, 1); /* child 消费 → app_on_key 不触发 */ usleep(80000); meow_input_inject(MEOW_EV_TAP, 10, 10); /* parent 消费 → app_on_tap 不触发 */