Files
MeowOS-For-R3P2/patch_v2/sdk/meow_ui_input.c
T
AskaEth 501867b362 feat(sdk): M6.39批 — LazyGrid/LazyColumn/LazyRow MD3容器化 + SDK全局App内底部上滑回桌面(系统手势优先)
- meow_ui_widget.c: W_LIST/meow_list/LazyColumn/LazyRow默认container=1(MD3圆角卡surfaceContainer+tonal选中+卡内按压+去分隔线) + 新增meow_list_set_container; 平坦回退set_container(0)
- meow_lazygrid.c: lg_draw_cell容器化(卡内缩8,surfaceContainer圆角+tonal选中+按压)
- meow_ui_input.c: 新增meow_ui_input_is_home_swipe(镜像np_home_gesture:屏底28px热区+上移65px+竖向主导,与左缘返回/列表中滑互斥) + MEOW_EV_HOME分发→meow_ui_nav_request_exit(系统保留手势**优先于App on_swipe**,任意App内生效→MSG_NAV EXIT→批①保活回桌面; 不调request_exit避免进程退出断送保活)
- 声明: meow_ui_input.h / meow_ui_widgets.h 各追加
- 宿主测试: test_meow_ui_home_gesture 19-0 / test_meow_ui_lazygrid 32-0, 0warning
2026-09-04 13:56:11 +08:00

888 lines
50 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* meow_ui_input.c — 输入层(B1.4)。
*
* 建立输入线程,读 /dev/input/event*(event1=触摸 goodix-ts、event0=物理键
* md-gpio-keys、event2=ADC 键),翻译成语义事件(KEY/TAP/SWIPE)压入环形队列;
* meow_ui_run / IME 循环弹出分发。应用不再自读 event*。
*
* 触摸:BTN_TOUCH 按下记录原点,抬起时位移 < 阈值 → TAP,否则按主导轴 → SWIPE。
* 物理键:read() 事件流 EV_KEY 值 1=按下 / 0=抬起(重复值 2 忽略)。M2.55/agent-217 起系统按键
* 语义在 SDK 输入层消化:
* 电源 116 → 维持现状(息屏/亮屏/长按菜单由内核/sys_server/watchdog),SDK 不动、不下发 APP;
* 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,volume_step/volume_max
* 约束,与 launcher 设置页/watchdog 同源);
* 播放/暂停 164、上一曲 165/19、下一曲 163 → 转发到当前媒体会话(App 是媒体源→本地回调,
* 否则经 IPC 发 launcher 转发)。
* 「App 收到的是语义事件」:上述音量/媒体键在 SDK 消化后**仍下发 MEOW_EV_KEY**(on_key),但
* App 不得把它们映射成自定义行为(APPS.md §3.4.1 语义不变)。
* 真机链路兜底(本次修复):物理键的按下不一定进入 evdev 事件流(事件流可被 EVIOCGRAB 独占或
* 驱动不投递;APPS.md §4.4 明确 launcher 用 EVIOCGKEY 状态轮询读键、「与 grab/投递无关」)。故
* 输入线程除 read() 事件流外,每轮还对键设备做一次 EVIOCGKEY 状态位图轮询,把 0→1/1→0 边沿并入
* 同一套系统键语义(poll_sys_keys_eviocgkey);事件流与位图双源经 sys_key_press/sys_key_release
* 的「同码 120ms 窗口」去重,一次物理按下只触发一步系统语义 + 一次 MEOW_EV_KEY 下发。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "meow_ui_sys.h"
#include "meow_ui_vol.h"
#include "meow_ui_media.h"
#include "meow_ui_view.h" /* M7.0a 缺口#7:把 meow_view_dispatch_event 接入 meow_ui_run */
#include "meow_ui_nav.h" /* 批②:底部上滑回桌面→meow_ui_nav_request_exit(MSG_NAV EXIT→批①保活) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <poll.h>
#include <pthread.h>
#include <sys/ioctl.h>
/* linux input 事件常量(本设备实测) */
#define EV_SYN 0
#define EV_KEY 1
#define EV_ABS 3
#define BTN_TOUCH 0x14a
#define ABS_X 0x00
#define ABS_Y 0x01
#define ABS_MT_POSITION_X 0x35
#define ABS_MT_POSITION_Y 0x36
#ifndef EVIOCGKEY
#define EVIOCGKEY(len) _IOC(_IOC_READ, 'E', 0x18, len)
#endif
#ifndef KEY_MAX
#define KEY_MAX 0x2ff
#endif
#define SWIPE_THRESH 65 /* px:抬起位移超过视为滑动(GESTURE_BACK: 90→65,用户实测 90 偏多,65 更顺手) */
#define EDGE_W 24 /* 左滑返回的「左缘起点」宽度(t_dx < EDGE_W 才认返回手势) */
#define HOME_EDGE_H 28 /* 批②:底部起滑区高(自屏底向上;镜像 np_gesture.h NP_HOME_EDGE_H) */
#define HOME_SWIPE 65 /* 批②:底部上滑位移阈值(镜像 np_gesture.h NP_HOME_SWIPE;= SWIPE_THRESH) */
#define MEOW_LONG_PRESS_MS 500u /* §6.3 通用长按默认阈值(ms;meow_ui_set_long_press_ms 可配) */
#define MEOW_LONG_PRESS_SLOP 12 /* §6.3 按下后「仍算按住」的最大位移(px);超此取消长按 */
#define QUEUE_N 128
#define MAX_DEVS 8
#define MEOW_APP_ACTIVITY_FILE "/tmp/meow_app_activity" /* M2.71-bug5:App 活动→launcher 重置息屏计时 */
/* §6.4 事件分发形式化:手势捕获终点前向声明(meow_input_drain 之前定义后置,见下方实现)。 */
void meow_ui_gesture_end(void);
/* ── 内部事件:系统级「下拉展开通知中心」(M2.5x 接线)──
* 触摸状态机在抬手判定处识别「SWIPE_DOWN 起始于顶部热区」后压入;事件循环按系统手势处理
* (路由 meow_ui_sys_notif_pull),不下发给 APP 的 on_swipe(顶部热区为系统保留手势)。
* 与左缘返回(SWIPE_LEFT)/ meow_ui_swipe_hint 互不影响——后者仍全屏生效,详见下方触摸机。 */
#define MEOW_EV_NOTIF_PULL 5
/* 批②:底部热区上滑 → 回桌面(后台保活,走批①链路)。系统手势,a=MEOW_SWIPE_UP(供
* on_swipe 接管、view 树消费),b=位移(px)。定义在 input.c 内部(同 MEOW_EV_NOTIF_PULL
* 未入公共头),宿主单测按同值声明(见 test_meow_ui_home_gesture.c)。 */
#define MEOW_EV_HOME 9
/* 顶部下滑 → 通知中心判定:纵向 DOWN 手势的起点(origin_y)是否落在顶部热区内。
* 热区由 meow_ui_statusbar_hotzone() 给出(非全屏=状态栏高、全屏=MEOW_SBAR_HOTZONE_FS、
* 收合=提示线高)。供宿主单测直调(判据与触摸状态机共用)。 */
int meow_ui_input_is_notif_pull(int origin_y, int hotzone)
{
return origin_y >= 0 && origin_y < hotzone;
}
/* 批② 底部热区上滑 → 回桌面(home)判定:镜像 launcher np_gesture.h np_home_gesture 同口径
* (热区 28px / 上移 ≥65px / 竖向主导 / 避开左缘返回列),SDK 侧 fb_h=屏幕高(复用
* meow_ui_height())。供宿主单测直调(判据与触摸状态机 touch_release 共用)。本判定把
* 「底部热区起滑 + 上移超阈值且竖向主导」从普通上滑中剥离——中部上滑/滚动仍归 App
* (LazyColumn/LazyGrid 滚动不受影响),底部热区的横向滑仍走原有左缘返回/横向逻辑。 */
int meow_ui_input_is_home_swipe(int down_x, int down_y, int dx, int dy, int fb_h)
{
int adx = dx < 0 ? -dx : dx;
if (down_x >= EDGE_W && down_y >= fb_h - HOME_EDGE_H && down_y >= 0 &&
dy <= -HOME_SWIPE && adx < -dy)
return 1;
return 0;
}
static int g_thread_started = 0;
static pthread_t g_thread;
static volatile int g_thread_run = 1;
/* 环形队列(互斥保护) */
static meow_input_ev g_q[QUEUE_N];
static volatile int g_qh = 0, g_qt = 0;
static pthread_mutex_t g_qm = PTHREAD_MUTEX_INITIALIZER;
/* 事件循环状态 */
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
static uint64_t now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */
static const char *meow_settings_path(void)
{
const char *e = getenv("MEOW_SETTINGS_FILE");
return (e && e[0]) ? e : "/usr/data/meow/settings.conf";
}
/* ---- 插孔消抖(M2.92,与 launcher/watchdog 255 同款)----
* 插拔 3.5/4.4 耳机的电气瞬态会在音量键 ADC(event2)上产生幽灵 VOL_UP/DOWN 按下
* (真机实证:插 4.4 同帧 ramp 爬满上限)。插孔状态变化后 800ms 内忽略音量键边沿。
* 节流:500ms 才真读 /sys 一次(不在热路径每圈读文件)。 */
static int g_jack_combo = -1; /* hs*2+bal 缓存,-1=未读 */
static unsigned long long g_jack_changed_at = 0, g_jack_polled_at = 0;
static int meow_input_jack_debounce(void)
{
unsigned long long now = meow_ui_time_ms();
if (now - g_jack_polled_at >= 500) {
g_jack_polled_at = now;
int hs = -1, bal = -1;
char b[8];
int fd = open("/sys/class/switch/headset/state", O_RDONLY);
if (fd >= 0) { ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n > 0) { b[n] = 0; hs = atoi(b); } }
fd = open("/sys/class/switch/balance/state", O_RDONLY);
if (fd >= 0) { ssize_t n = read(fd, b, sizeof b - 1); close(fd); if (n > 0) { b[n] = 0; bal = atoi(b); } }
if (hs >= 0 && bal >= 0) {
int combo = hs * 2 + bal;
if (g_jack_combo >= 0 && combo != g_jack_combo) g_jack_changed_at = now;
g_jack_combo = combo;
}
}
return g_jack_changed_at && (now - g_jack_changed_at) < 800;
}
/* ---- §10.5 叠加让位:通知中心叠加会话期 / 批①后台常驻期 输入排空(播放中 App 不被 SIGSTOP 的配套)----
* 媒体播放中的 App 前台唤起通知中心时 launcher 不冻结 App(保 PCM 供流,ncm 播放线程同进程),
* App 输入线程仍在读同一批 evdev 设备(每 fd 各收一份事件副本,与 launcher 互不剥夺)——若不
* 抑制,面板上的一次点击会被 App 再消费一次(如面板媒体卡片「暂停」与 App 同坐标控件双重触发);
* 批①后台常驻 App 同理:前台 App(如 Hello)时,后台 ncm 的触摸必须被排空(一次交互只响应前台)。
* 判据统一走 meow_ui_overlay_suppressed_active()(内容=前台 app_id;非前台/空=抑制)。抑制期:
* 排空本进程 fd 队列并丢弃、EVIOCGKEY 位图静默对齐(不产生边沿语义,系统键动作叠加期由
* launcher/watchdog 单侧承担)。200ms 节流缓存由共享判定承担(与 present 层同口径)。 */
static int overlay_input_suppressed(void)
{
return meow_ui_overlay_suppressed_active();
}
/* ---- 系统按键语义(M2.55:SDK 输入层消化,App 前台也生效)----
* 物理键是系统资源(APPS.md §3.4.1),语义全局统一。本函数在输入线程执行系统动作:
* 电源 116 → 息屏/亮屏/长按菜单(内核/sys_server/watchdog),SDK 不动(维持现状);
* 音量 114/115 → 调系统音量(meow_settings audio.volume + CS43198 DAC 写,与 launcher
* 设置页/watchdog 同源:volume_step/volume_max 约束),仅按下边沿(1)一步;
* 播放/上次曲 164/165/19/163 → 转发到当前媒体会话(App 是媒体源→本地回调;否则 IPC→launcher)。
* 仅处理按下边沿(val==1)触发系统动作;重复(2)/抬起(0)不重复执行。on_key 的下发由输入线程
* 在调用本函数后另行 push(音量/媒体键仍通知 App,App 不得映射)。 */
static void meow_input_handle_sys_key(int code, int val)
{
switch (code) {
case MEOW_KEY_POWER: /* 电源:维持现状,SDK 不动 */
break;
case MEOW_KEY_VOLUMEDOWN:
case MEOW_KEY_VOLUMEUP:
if (val == 1 && !meow_input_jack_debounce()) { /* 插孔瞬态期忽略幽灵音量边沿 */
int dir = (code == MEOW_KEY_VOLUMEUP) ? +1 : -1;
int nv = meow_ui_vol_step(meow_settings_path(), dir); /* settings 语义+持久化 */
meow_ui_vol_dac_write(nv); /* 写真实 CS43198 DAC */
}
break;
case MEOW_KEY_PLAYPAUSE:
case MEOW_KEY_PREVIOUS:
case MEOW_KEY_NEXT:
case 19: /* KEY_PREVIOUSSONG 旧码 */
if (val == 1) meow_ui_media_sys_key(code);
break;
default:
break;
}
}
void meow_input_drain(void)
{
pthread_mutex_lock(&g_qm);
g_qh = 0; g_qt = 0;
pthread_mutex_unlock(&g_qm);
meow_ui_gesture_end(); /* §6.4:清空队列=丢弃未完成手势(如开 IME/模态前),释放捕获防悬垂 */
}
static void push_ev(int type, int a, int b)
{
pthread_mutex_lock(&g_qm);
int n = (g_qt + 1) % QUEUE_N;
if (n == g_qh) { /* 满:丢弃 */
pthread_mutex_unlock(&g_qm);
return;
}
g_q[g_qt].type = (uint8_t)type; g_q[g_qt].a = a; g_q[g_qt].b = b;
g_qt = n;
pthread_mutex_unlock(&g_qm);
}
int meow_input_pop(meow_input_ev *ev)
{
if (!ev) return 0;
pthread_mutex_lock(&g_qm);
if (g_qh == g_qt) { pthread_mutex_unlock(&g_qm); return 0; }
*ev = g_q[g_qh];
g_qh = (g_qh + 1) % QUEUE_N;
pthread_mutex_unlock(&g_qm);
return 1;
}
void meow_input_inject(int type, int a, int b)
{
push_ev(type, a, b);
}
/* ---- 触摸状态机(M2.22 B:手势级干净起点 + 原点严格锁定,杜绝残留坐标污染)----
* 背景:goodix 事件时序不保证 BTN_TOUCH 与 ABS 的先后;旧版在按下边沿用 t_cx/t_cy
* 立即锁原点,而 t_cx/t_cy 是上一手势的残留 —— 上一次「中部 tap」留下的坐标被本次
* 按下当原点(>EDGE_W),导致后续左缘侧滑不返回、进度条不出。且进程内不自愈。
* 修法:每次抬手彻底清空全部轨迹状态;原点只在「本次按下之后」出现的首个有效 ABS
* 锁定(touch_update,gated 于 t_down && !t_origin_valid && x>=0 && y>=0)。 */
static int t_down = 0;
static int t_press_valid = 0; /* BTN_TOUCH=1 已见(本次按下的 gate),等首个有效 ABS 才锁原点 */
static int t_dx = 0, t_dy = 0; /* 按下原点(BTN_TOUCH 后首个有效 ABS 锁定) */
static int t_cx = 0, t_cy = 0; /* 当前坐标(-1=本次尚未见到有效坐标) */
static int t_origin_valid = 0; /* 原点是否已锁定 */
static uint64_t t_down_ms = 0; /* 按下时刻(monotonic ms),供实时按住时长用 */
/* M2.71-bug2/6:拖动过程事件上次上报坐标(坐标变化才入队 MEOW_EV_TOUCH_MOVE,防刷屏)。 */
static int t_mv_x = -1, t_mv_y = -1;
/* §6.3 通用长按:本次按压是否已触发过长按(触发后抬手不再产生 TAP/SWIPE)。 */
static int t_long_fired = 0;
/* §6.3 通用长按:阈值(默认 MEOW_LONG_PRESS_MS;App 可经 meow_ui_set_long_press_ms 配)。 */
static int g_long_press_ms = MEOW_LONG_PRESS_MS;
/* §6.3 注册的长按回调(NULL=未注册;触发时以 (x,y) 调用,坐标=触发时刻按下点)。 */
static meow_long_press_fn g_long_press_cb = NULL;
/* 按下边沿:只标记按下,绝不用传入坐标立即锁原点(避免用上一手势残留坐标当原点)。
* t_cx/t_cy 归 -1 —— 本次手势以「无坐标」起步,只接受随后到来的有效 ABS。 */
static void touch_press(int x, int y)
{
(void)x; (void)y;
t_down = 1;
t_press_valid = 1;
t_origin_valid = 0;
t_cx = -1; t_cy = -1;
t_dx = 0; t_dy = 0;
t_down_ms = now_ms(); /* 记按下时刻:供实时按住时长/按住视觉 */
t_mv_x = -1; t_mv_y = -1; /* M2.71:拖动过程起点未定 */
t_long_fired = 0; /* §6.3:每次新按压复位长按触发标记 */
}
/* 由 ABS_X/ABS_Y 驱动:若本次按下且原点未锁且坐标有效 → 锁原点;再更新当前坐标。
* M2.71-bug2/6:原点锁定即报 MEOW_EV_TOUCH_DOWN(拖动起点);随后坐标变化报 MEOW_EV_TOUCH_MOVE
* (坐标变了才报,省队列)。这两类事件供 slider 拖动 / 列表&页面滚动跟手。 */
static void touch_update(int x, int y)
{
if (t_down && !t_origin_valid && x >= 0 && y >= 0) {
t_dx = x; t_dy = y; t_origin_valid = 1;
t_mv_x = x; t_mv_y = y;
push_ev(MEOW_EV_TOUCH_DOWN, x, y);
}
if (x >= 0) t_cx = x;
if (y >= 0) t_cy = y;
if (t_down && t_origin_valid && t_cx >= 0 && t_cy >= 0 &&
(t_cx != t_mv_x || t_cy != t_mv_y)) {
t_mv_x = t_cx; t_mv_y = t_cy;
push_ev(MEOW_EV_TOUCH_MOVE, t_cx, t_cy);
}
}
static void touch_release(void)
{
if (!t_down) return;
t_down = 0; t_press_valid = 0;
int was_long = t_long_fired; /* §6.3:本次按压是否已触发过长按(抬手不再产生点击/滑动) */
t_long_fired = 0;
/* 本次按下从未锁到有效坐标(幽灵触摸/坐标未就绪):丢弃,且清空全部轨迹。 */
if (!t_origin_valid) { t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1; return; }
t_origin_valid = 0;
int mdx = t_cx - t_dx, mdy = t_cy - t_dy;
int adx = mdx < 0 ? -mdx : mdx, ady = mdy < 0 ? -mdy : mdy;
/* 长按已触发:本次手势由长按消费,抬手不再产生 TAP/SWIPE/NOTIF_PULL(抑制点击,§6.3)。 */
if (was_long) {
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
return;
}
/* 手势判定只在「抬手(release)边沿」做:按下/移动过程不触发 SWIPE,仅报 PROGRESS。
*
* 返回手势("左滑"语义):原点确实在左缘 + 向右移动 ≥SWIPE_THRESH。设备实际为
* 「左缘向内(右)划」≥65px(M2.21:90→65 用户实测更顺手;与 watchdog 兜底一致)。
* 原点 t_dx 是本次按下后首个有效 ABS 锁定的真实按下点(非残留),且 t_cx/t_dx 已
* 被手势级清空,绝不继承上一手势坐标,确保:
* ① 中部右滑不会误判成左缘返回(t_dx>=EDGE_W);
* ② 先点中部再侧滑不返回(原点锁定在中部,>=EDGE_W),符合「必须从左缘起划」;
* ③ 右移严格正向(t_cx-t_dx>=0),杜绝「向左划也出条/误返回」。
* SDK 统一上报为 MEOW_SWIPE_LEFT(设计"左滑=返回"契约,hello 用 on_swipe(LEFT) 退出)。
*
* 屏幕中部滑动不是返回手势:中部右滑→RIGHT(内容导航),中部左滑→非返回(落 TAP,
* 避免任意处左滑误退出)。纵向滑动→UP/DOWN 照旧。 */
if (adx >= SWIPE_THRESH || ady >= SWIPE_THRESH) {
/* 顶部下滑 → 通知中心(系统保留手势,不下发 APP):起点在顶部热区 + 有真实向下位移。
* 在主导轴判定(adx vs ady)之前先行识别并置最高优先级——顶起「起点同时落在顶部热区与
* 左缘」的边界(定案:起点 y 在顶部热区 → 顶部手势优先)。若无此优先,下落时含右向分量的
* 手势会被水平分量抢先判成 MEOW_SWIPE_LEFT → 漏给 App on_swipe → 根页退出(真机闪退链)。
* 热区高度随状态变化(非全屏=整条状态栏、全屏=MEOW_SBAR_HOTZONE_FS 缩小、收合=提示线高)
* ——全屏 app 只在顶部极窄热区被系统占用,故「不影响全屏 app」。
* 仅当真「向下拉」(mdy>0 且 ady≥SWIPE_THRESH)才认:纯水平右滑(左缘返回)或上滑不误判。 */
if (mdy > 0 && ady >= SWIPE_THRESH &&
meow_ui_input_is_notif_pull(t_dy, meow_ui_statusbar_hotzone())) {
push_ev(MEOW_EV_NOTIF_PULL, t_dy, ady > adx ? ady : adx);
} else if (adx >= ady) {
if (t_dx >= 0 && t_dx < EDGE_W && (t_cx - t_dx) >= SWIPE_THRESH) {
push_ev(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, adx); /* 左缘返回手势 */
} else if (mdx > 0) {
push_ev(MEOW_EV_SWIPE, MEOW_SWIPE_RIGHT, adx); /* 中部右滑:内容 */
} else {
push_ev(MEOW_EV_TAP, t_cx, t_cy); /* 中部左滑:非返回 */
}
} else {
int dir = mdy > 0 ? MEOW_SWIPE_DOWN : MEOW_SWIPE_UP;
int dist = ady > adx ? ady : adx;
/* 批②:竖向主导且上移(dir==UP)时,判「底部热区上滑 → 回桌面」。仅当起点在底部
* 热区(最后 HOME_EDGE_H px)且**不在左缘**(down_x>=EDGE_W,与左缘返回互斥)且
* 上移 ≥HOME_SWIPE 且竖向主导(adx<-dy)才命中;否则照常 SWIPE_UP(中部上滑/滚动交
* 由 App)。命中改入队 MEOW_EV_HOME(区别于普通上滑,供 run 循环按兜底回桌面)。 */
if (dir == MEOW_SWIPE_UP &&
meow_ui_input_is_home_swipe(t_dx, t_dy, mdx, mdy, meow_ui_height()))
push_ev(MEOW_EV_HOME, MEOW_SWIPE_UP, dist);
else
push_ev(MEOW_EV_SWIPE, dir, dist);
}
} else {
push_ev(MEOW_EV_TAP, t_cx, t_cy);
}
/* 手势级干净起点:清空全部轨迹,下一手势从零开始,杜绝污染无自愈。 */
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
}
/* ---- 实时触摸态(内部:控件绘制按压/按住视觉用)----
* 直接读触摸状态机当前值(与语义事件队列无关;无按下/坐标未就绪时 x/y 返回 -1)。 */
int meow_input_touch_down(void) { return t_down; }
int meow_input_touch_x(void) { return t_cx; }
int meow_input_touch_y(void) { return t_cy; }
unsigned int meow_input_touch_hold_ms(void)
{
if (!t_down) return 0;
uint64_t e = now_ms() - t_down_ms;
if (e > 0xFFFFFFFFu) e = 0xFFFFFFFFu;
return (unsigned int)e;
}
/* 宿主单测/内部注入:模拟按压(down=1 时 x,y=压点、hold_ms=已按住时长),设备运行时无调用方。 */
void meow_input_touch_set(int down, int x, int y, unsigned hold_ms)
{
if (down) {
t_down = 1; t_press_valid = 1; t_origin_valid = 1; t_long_fired = 0;
t_cx = x; t_cy = y; t_dx = x; t_dy = y;
uint64_t now = now_ms();
t_down_ms = (now > (uint64_t)hold_ms) ? (now - (uint64_t)hold_ms) : 0;
} else {
t_down = 0; t_press_valid = 0; t_origin_valid = 0; t_long_fired = 0;
t_cx = -1; t_cy = -1; t_dx = 0; t_dy = 0; t_down_ms = 0;
t_mv_x = -1; t_mv_y = -1;
}
}
/* 宿主单测/内部注入:模拟按下后移动到 (x,y)(保原点、更新当前坐标;位移超长按容忍即取消)。
* 对应输入线程 touch_update 的位移分支(这里不产生语义事件,仅更新坐标状态)。 */
void meow_input_touch_move(int x, int y)
{
if (!t_down || !t_origin_valid) return;
t_cx = x; t_cy = y;
t_mv_x = x; t_mv_y = y;
}
/* 宿主单测/内部注入:模拟一次抬手(真实 touch_release;含长按后抑制 TAP/SWIPE)。 */
void meow_input_touch_release(void) { touch_release(); }
/* ---- §6.3 通用长按公共 API + 阈值跨越检测 ----
* meow_input_long_press_poll:主循环(meow_ui_run 每帧)与宿主单测调用。读触摸状态机:
* 按下、原点/坐标已就绪、未触发过长按、位移 ≤ 容忍、按住时长 ≥ 阈值 → 置 t_long_fired,
* 入队 MEOW_EV_LONG_PRESS(a=x,b=y=当前按下点),返回 1;否则返回 0。幂等(一次按压一次)。 */
void meow_ui_set_long_press_cb(meow_long_press_fn cb) { g_long_press_cb = cb; }
void meow_ui_set_long_press_ms(int ms)
{ g_long_press_ms = (ms > 0) ? ms : (int)MEOW_LONG_PRESS_MS; }
int meow_ui_long_press_ms(void) { return g_long_press_ms; }
int meow_input_long_press_poll(void)
{
if (!g_long_press_cb) return 0; /* 未注册回调:长按整体关闭,保持旧点击语义(短按/长按都 TAP) */
if (!t_down || t_long_fired || !t_origin_valid || t_cx < 0 || t_cy < 0) return 0;
int mdx = t_cx - t_dx, mdy = t_cy - t_dy;
int amx = mdx < 0 ? -mdx : mdx, amy = mdy < 0 ? -mdy : mdy;
int move = amx > amy ? amx : amy;
if (move > MEOW_LONG_PRESS_SLOP) return 0; /* 位移超容忍:取消 */
if (meow_input_touch_hold_ms() < (unsigned)g_long_press_ms) return 0; /* 未达阈值 */
t_long_fired = 1;
push_ev(MEOW_EV_LONG_PRESS, t_cx, t_cy);
return 1;
}
/* ============================================================ */
/* §6.4 事件分发形式化:模态作用域 / 自上而下命中 / 手势捕获 / 唯一焦点
* ------------------------------------------------------------
* 统一仲裁者:把 ad-hoc 的「命中/捕获/消费/层间隔离/焦点」收敛为单一模型。
* 兼容:本节 API 只追加;未调用者行为与之前完全一致(命中/捕获仍由 App 自理)。 */
/* ============================================================ */
/* ---- 模态作用域栈(弹层叠开,depth 递增;存在则下层完全收不到触摸)---- */
#define MEOW_MODAL_SCOPE_MAX 8
static void *g_modal_stack[MEOW_MODAL_SCOPE_MAX];
static int g_modal_depth = 0;
static char g_modal_sentinel; /* 「模态哨兵」地址:模态存在时 hit_topdown 的返回占位 */
meow_ui_modal *meow_ui_modal_begin(void)
{
if (g_modal_depth >= MEOW_MODAL_SCOPE_MAX) return NULL; /* 栈满:不弹(骨架级静默) */
void *h = &g_modal_stack[g_modal_depth];
g_modal_stack[g_modal_depth++] = h;
return (meow_ui_modal *)h;
}
void meow_ui_modal_end(meow_ui_modal *m)
{
if (!m) return;
for (int i = 0; i < g_modal_depth; i++)
if (g_modal_stack[i] == (void *)m) {
g_modal_depth = i; /* 弹到 m(含),一次性关上嵌套上层 */
return;
}
}
int meow_ui_modal_active(void) { return g_modal_depth > 0; }
int meow_ui_modal_depth(void) { return g_modal_depth; }
/* ---- 自上而下命中(模态门控 + z 降序控件命中)---- */
void *meow_ui_hit_topdown(int x, int y)
{
if (meow_ui_modal_active()) return &g_modal_sentinel; /* 模态存在:恒由模态/背板消费,不透传 */
return meow_draw_hit_topdown(x, y);
}
int meow_ui_widget_hit(meow_widget *w, int x, int y) { return meow_widget_hit(w, x, y); }
/* ---- 手势捕获(down 捕获直到 up)---- */
static void *g_capture = NULL;
void *meow_ui_gesture_begin(int x, int y)
{
void *t = meow_ui_hit_topdown(x, y);
g_capture = t; /* 覆盖旧捕获(一次手势至多一个目标;命中 NULL=未捕获,move/up 照旧) */
return g_capture;
}
void *meow_ui_gesture_owner(void) { return g_capture; }
void meow_ui_gesture_end(void) { g_capture = NULL; }
/* ---- 唯一焦点(list 焦点残留根治 / 滚动容器间转移)---- */
static void *g_focus = NULL;
static meow_ui_focus_lost_fn g_focus_lost = NULL;
static void *g_focus_lost_ctx = NULL;
void meow_ui_focus_set_lost_cb(meow_ui_focus_lost_fn cb, void *ctx)
{ g_focus_lost = cb; g_focus_lost_ctx = ctx; }
void *meow_ui_focus_owner(void) { return g_focus; }
int meow_ui_focus_acquire(void *owner)
{
if (!owner) return 0;
void *prev = g_focus;
g_focus = owner;
if (prev && prev != owner && g_focus_lost) g_focus_lost(prev, g_focus_lost_ctx);
return 1;
}
int meow_ui_focus_release(void *owner)
{
if (!g_focus) return 0;
if (owner && owner != g_focus) return 0;
void *prev = g_focus;
g_focus = NULL;
if (g_focus_lost) g_focus_lost(prev, g_focus_lost_ctx);
return 1;
}
void meow_ui_focus_tap(int x, int y)
{
if (!g_focus) return;
void *hit = meow_ui_hit_topdown(x, y);
if (hit != g_focus) meow_ui_focus_release(g_focus); /* 点非焦点区 → 释放并通知 */
}
/* ---- 系统键(音量/媒体)去重 + EVIOCGKEY 状态轮询 ----
* M2.55/agent-217 之后 SDK 在输入线程消化系统键语义;但真机物理键(jz adc keyboard event2、
* md-gpio-keys event0、earpods_adc event3)的按下不一定进入 evdev 事件流(事件流可被其它进程
* EVIOCGRAB 独占或驱动不投递),而 launcher 全程用 EVIOCGKEY 状态轮询读键(APPS.md §4.4,
* 「与 grab/投递无关」)。为让 App 前台物理音量/媒体键可靠生效,输入线程除 read() 事件流外,
* 还按固定节奏轮询内核按键位图(EVIOCGKEY),把 0→1/1→0 边沿并入同一套系统键语义。
* 双源(事件流 + 状态位图)对同一次物理按下各报一次,用「同码 120ms 窗口」去重,保证
* 一次按下只触发一步系统语义 + 一次 MEOW_EV_KEY 下发(App on_key 收到语义事件,不得映射)。 */
static int g_sys_press_code = -1;
static uint64_t g_sys_press_ms = 0;
#define SYS_KEY_DEDUP_MS 120
static void sys_key_press(int code)
{
uint64_t now = now_ms();
if (code == g_sys_press_code && now - g_sys_press_ms < SYS_KEY_DEDUP_MS)
return; /* 事件流 + EVIOCGKEY 双源去重:同码窗口内只消化一次 */
g_sys_press_code = code;
g_sys_press_ms = now;
meow_input_handle_sys_key(code, 1);
push_ev(MEOW_EV_KEY, code, 1);
}
static void sys_key_release(int code)
{
push_ev(MEOW_EV_KEY, code, 0);
}
/* 对一个 event 设备做一次 EVIOCGKEY 状态边沿检测(prev 为该设备上一状态位图,原地更新)。
* 只在系统键码(音量/媒体)上找边沿;电源(116)不进此表——SDK 维持「不动、不下发」纪律。 */
static void poll_sys_keys_eviocgkey(int fd, unsigned char *prev)
{
unsigned char cur[KEY_MAX / 8 + 1];
if (ioctl(fd, EVIOCGKEY(sizeof cur), cur) < 0) return;
static const int sys_codes[] = {
MEOW_KEY_VOLUMEDOWN, MEOW_KEY_VOLUMEUP, MEOW_KEY_PLAYPAUSE,
MEOW_KEY_PREVIOUS, MEOW_KEY_NEXT, 19,
};
for (unsigned ci = 0; ci < sizeof sys_codes / sizeof sys_codes[0]; ci++) {
int code = sys_codes[ci];
int b = (cur[code / 8] >> (code % 8)) & 1;
int pb = (prev[code / 8] >> (code % 8)) & 1;
if (b == pb) continue;
prev[code / 8] = (unsigned char)(b ? (prev[code / 8] | (1u << (code % 8)))
: (prev[code / 8] & (unsigned char)~(1u << (code % 8))));
if (b) sys_key_press(code); /* 0→1:按下(去重后触发一步系统语义) */
else sys_key_release(code); /* 1→0:抬起 */
}
}
/* ---- 输入线程:poll 所有 event 设备 + EVIOCGKEY 系统键轮询 ---- */
static void *input_thread(void *arg)
{
(void)arg;
struct pollfd fds[MAX_DEVS];
unsigned char prev_key[MAX_DEVS][KEY_MAX / 8 + 1];
int nfd = 0;
char dev[32];
for (int i = 0; i < MAX_DEVS; i++) {
snprintf(dev, sizeof dev, "/dev/input/event%d", i);
int e = open(dev, O_RDONLY | O_NONBLOCK);
if (e >= 0) {
fds[nfd].fd = e; fds[nfd].events = POLLIN; fds[nfd].revents = 0;
/* EVIOCGKEY 上一状态初始化为当前按下位图:启动时已按住的键不误判为新按下 */
memset(prev_key[nfd], 0, sizeof prev_key[nfd]);
if (ioctl(e, EVIOCGKEY(sizeof prev_key[nfd]), prev_key[nfd]) < 0)
memset(prev_key[nfd], 0, sizeof prev_key[nfd]);
nfd++;
}
}
int overlay_was = 0; /* §10.5:上一圈是否处于叠加抑制(进入沿复位触摸状态机用) */
while (g_thread_run) {
if (nfd == 0) { usleep(20000); continue; }
int r = poll(fds, nfd, 20);
/* §10.5 叠加让位:通知中心叠加会话期(标记存在)排空并丢弃本进程事件副本——面板输入
* 由 launcher 接管,App 不得再消费(防面板点击被 App 双重触发);EVIOCGKEY 位图静默
* 对齐(不产生边沿语义/系统键动作,解除后不会把面板期按键误判为新边沿)。 */
if (overlay_input_suppressed()) {
if (!overlay_was) { /* 进入沿:复位触摸状态机,防抑制期丢失抬手致 t_down 卡死 */
overlay_was = 1;
t_down = 0; t_press_valid = 0; t_origin_valid = 0;
t_cx = t_cy = -1; t_dx = t_dy = 0; t_down_ms = 0; t_mv_x = t_mv_y = -1;
t_long_fired = 0;
}
if (r > 0)
for (int i = 0; i < nfd; i++) {
if (!(fds[i].revents & POLLIN)) continue;
unsigned char junk[64];
while (read(fds[i].fd, junk, sizeof junk) > 0) { /* 整批丢弃 */ }
}
for (int i = 0; i < nfd; i++) {
unsigned char cur[KEY_MAX / 8 + 1];
if (ioctl(fds[i].fd, EVIOCGKEY(sizeof cur), cur) == 0)
memcpy(prev_key[i], cur, sizeof cur);
}
continue;
}
overlay_was = 0;
if (r < 0) {
if (errno != EINTR) usleep(20000);
} else if (r > 0) {
for (int i = 0; i < nfd; i++) {
if (!(fds[i].revents & POLLIN)) continue;
unsigned char ev[16];
while (read(fds[i].fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
uint16_t type = *(uint16_t *)(ev + 8);
uint16_t code = *(uint16_t *)(ev + 10);
int32_t val = *(int32_t *)(ev + 12);
if (type == EV_ABS) {
if (code == ABS_X || code == ABS_MT_POSITION_X) {
if (t_down) touch_update(val, t_cy); /* 锁原点+更新当前(t_down 时才驱动) */
else t_cx = val;
/* 左滑进度实时上报(视觉条):原点确实在左缘(t_dx<EDGE_W) 且 向右移动
* (t_cx>t_dx)——grow/回缩全程由「非全屏已验证的同一条路径」驱动,全屏仅
* 垂直位置 hy 不同(hy=meow_ui_statusbar_height()),别无全屏特判。
* 抬手触发仍由 touch_release 判定;这里只报过程(事件载荷 b=当前X)。
* t_dx 是本次按下后首个有效 ABS 锁定的真实按下点(非残留),且每次抬手已
* 清空,杜绝「中部点一下出条 / 向左划也出条」。 */
if (t_down && t_origin_valid && t_dx >= 0 && t_dx < EDGE_W && t_cx > t_dx)
push_ev(MEOW_EV_SWIPE_PROGRESS, t_dx, t_cx);
}
else if (code == ABS_Y || code == ABS_MT_POSITION_Y) {
if (t_down) touch_update(t_cx, val); else t_cy = val;
}
} else if (type == EV_KEY) {
if (code == BTN_TOUCH) {
if (val && !t_down) touch_press(t_cx, t_cy);
else if (!val && t_down) touch_release();
} else if (code == MEOW_KEY_POWER) {
/* M2.55(接 M2.24 纪律):电源=息屏/亮屏/长按菜单,由内核/sys_server/
* watchdog 统一处理,SDK 不动、也不下发 APP。维持现状。 */
} else if (code == MEOW_KEY_VOLUMEDOWN || code == MEOW_KEY_VOLUMEUP ||
code == MEOW_KEY_PLAYPAUSE || code == MEOW_KEY_PREVIOUS ||
code == MEOW_KEY_NEXT || code == 19) {
/* 系统键(音量/媒体):真实物理键可能不进事件流,另经 EVIOCGKEY 状态轮询
* 兜底(poll_sys_keys_eviocgkey);双源经 sys_key_press/sys_key_release 去重,
* 一次按下只一步系统语义 + 一次 MEOW_EV_KEY 下发(App on_key 不得映射)。 */
if (val == 1) sys_key_press(code);
else if (val == 0) sys_key_release(code);
} else if (val == 1) { /* 按下边沿(忽略重复=2)——仅未知键(本设备无) */
push_ev(MEOW_EV_KEY, code, 1);
} else if (val == 0) { /* 抬起 */
push_ev(MEOW_EV_KEY, code, 0);
}
}
}
}
}
/* EVIOCGKEY 系统键状态轮询:无论事件流是否投递都跑(真实物理键可能不进事件流,
* 与 launcher kp_poll 同源的按键位图读取兜底;事件流 + 位图双源在 sys_key_press 去重)。 */
for (int i = 0; i < nfd; i++)
poll_sys_keys_eviocgkey(fds[i].fd, prev_key[i]);
}
for (int i = 0; i < nfd; i++) close(fds[i].fd);
return NULL;
}
void meow_input_start(void)
{
if (g_thread_started) return;
g_thread_started = 1;
g_thread_run = 1;
pthread_create(&g_thread, NULL, input_thread, NULL);
}
/* ---- 事件循环 ---- */
void meow_ui_request_exit(void) { g_exit = 1; }
/* M2.71-bug5:App 活动信号(节流 ≤2/s 写标记文件,限 I/O)。
* 仅真机写(宿主/qemu 无 launcher 消费,避免噪音);launcher 前台 app 循环消费它重置息屏计时。 */
static uint64_t g_activity_last_ms = 0;
void meow_ui_activity_ping(void)
{
uint64_t now = now_ms();
if (now - g_activity_last_ms < 500) return; /* 节流:任意交互连续触摸也只 ~2 次/s */
g_activity_last_ms = now;
if (!meow_gfx_is_device()) return; /* 离屏/宿主:无 launcher 消费 */
int fd = open(MEOW_APP_ACTIVITY_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { if (write(fd, "1", 1) < 0) { } close(fd); }
}
void meow_ui_run(const meow_ui_events *ev)
{
if (ev) { g_events = *ev; g_events_set = 1; }
g_exit = 0;
meow_input_start();
last_tick_ms = now_ms();
startup_cooldown_until = now_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */
while (!g_exit) {
/* agent-119:实时触摸按压/按住/释放 → 即时重绘命中按钮区(不等 app 重绘)。
* IME 打开时软键盘为覆盖层且拦截触摸,不驱动背后按钮视觉,跳过。
* §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动;启动窗口不触发)。 */
if (!meow_ime_active()) {
meow_widget_touch_refresh();
if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll();
} else if (meow_ime_mode_pinyin()) {
/* 拼音 IME 打开:喂长按轮询(长按退格清空字段)。英文模式不喂(保持既有行为)。 */
if (now_ms() >= startup_cooldown_until) meow_input_long_press_poll();
}
meow_input_ev e;
int got = 0;
while (meow_input_pop(&e)) {
if (g_exit) break; /* 已请求退出:不再分发剩余事件,杜绝「退出后又触发」的偶发再进/退 */
got = 1;
meow_ui_activity_ping(); /* M2.71-bug5:任意交互事件都喂 launcher 重置息屏计时(节流) */
int in_startup = (now_ms() < startup_cooldown_until); /* M2.22(C) */
if (e.type == MEOW_EV_TAP) {
if (in_startup) continue; /* 启动窗口也吞 TAP:旧手势残留点按(如上一次左滑抬起)
* 在进入瞬间命中 UI 按钮(如全屏按钮)会误触发——丢弃。 */
/* 抬手未达标:清视觉条 */
meow_ui_swipe_hint(-1);
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
/* §6.4 手势结束(抬手边沿)+ 焦点点外释放:先释放手势捕获(move/up 已结束),
* 再对落点做「命中焦点所有者→保位;否则释放并通知」(list 焦点残留根治)。 */
meow_ui_gesture_end();
meow_ui_focus_tap(e.a, e.b);
/* §6.1 触摸分流:③ 系统全局(IME/状态栏…已在上面消费) → ② App 全局控件
* (命中返回 1=消费,否则落到 ① active Activity 页面控件)。 */
if (meow_ui_global_hit(e.a, e.b)) continue;
/* M7.0a 缺口#7:meow_view 事件路由(§A.7)——有活动页 view 子树且其 on_event
* 命中即消费(不再落 App on_tap);无 view 树/未命中返回 0 → 回落 on_tap 原路径。 */
if (meow_view_dispatch_event(MEOW_EV_TAP, e.a, e.b)) continue;
if (g_events_set && g_events.on_tap) g_events.on_tap(e.a, e.b);
} else if (e.type == MEOW_EV_KEY) {
/* M7.0a 缺口#7:先进 view 路由,命中即消费;否则回落 App on_key(语义不变——
* 音量/媒体系统键已在输入线程消化,这里只决定 App/页面是否收到语义事件)。 */
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; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */
/* 抬手达标:清视觉条(左滑返回),触发行消失 */
meow_ui_swipe_hint(-1);
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
/* 拼音 IME 候选栏横滑翻页(英文模式 handle_event 返回 0 → 沿用既有行为)。 */
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
meow_ui_gesture_end(); /* §6.4:抬手(滑动)边沿,释放手势捕获 */
/* M7.0a 缺口#7:view 路由先于 App 窗口的 on_swipe 与默认返回手势——页 view
* 可消费滑动(页内返回/翻页/横幅);命中即跳过下面对 on_swipe/默认退出的分发。 */
if (meow_view_dispatch_event(MEOW_EV_SWIPE, e.a, e.b)) continue;
/* A-0 全局左缘返回(方案A):
* - APP 注册 on_swipe → APP 接管,自行决定(meow_ui_request_exit / 页内返回);
* - APP 未注册 on_swipe 且非全屏(sb_active)→ SDK 默认退出 app(返回=退出);
* - 全屏/沉浸 → 无系统返回手势,仅 APP 自己处理。
* 扩展点:阶段D M3 导航栈落地后,「默认退出」升级为「按 nav_stack 回退」。 */
int sb_active = meow_ui_sys_statusbar_active();
if (g_events_set && g_events.on_swipe) {
g_events.on_swipe(e.a, e.b, 0);
} else if (e.a == MEOW_SWIPE_LEFT && sb_active) {
meow_ui_request_exit();
}
} else if (e.type == MEOW_EV_HOME) {
/* 系统级「底部热区上滑回桌面」(用户 2026-09-04 批②补全,任意 App 内生效):
* 屏底边缘热区+竖向主导上滑已由 meow_ui_input_is_home_swipe 过滤(与 App 中部
* 滚动、底部横向滑、左缘返回互斥),故作为**系统保留手势**优先于 App on_swipe /
* view 树——否则现有 App(ncm/hello/files/reader 均注册 on_swipe)会接管导致
* App 内上滑不回桌面。命中调 meow_ui_nav_request_exit()(MSG_NAV EXIT → launcher
* 批① meow_bg_add 保活,进程保留转后台、SDK present 自抑);**不**调
* meow_ui_request_exit()——那会断送保活(进程 exit(0))。launcher 据 app 声明
* (background_keepalive/background_audio+媒体活跃)决定无限期/宽限。 */
if (in_startup) continue; /* 启动窗口误触残留:丢弃 */
meow_ui_swipe_hint(-1);
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
/* IME 打开时软键盘覆盖底部:键盘拥有底部区,把事件交给 IME 且**绝不**回桌面
* (防输入时误退)。 */
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; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */
/* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发
* APP on_swipe;路由到系统钩子(App 进程只检测+经 IPC 上报 MSG_NOTIF_OPEN,
* 通知中心由 launcher 渲染,见 FRAMEWORK_DESIGN §10.5)。
* 起点若同时落在左缘,拖动期可能已画过 MEOW_EV_SWIPE_PROGRESS 左缘指示条,
* 抬手定判为 NOTIF_PULL 后须清掉(否则通知中心打开后残留返回条)。 */
meow_ui_swipe_hint(-1);
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
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)期间本主循环被阻塞,天然不在此分发。
* 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_* 不受影响)。 */
if (e.type == MEOW_EV_TOUCH_DOWN) meow_ui_gesture_begin(e.a, e.b);
if (g_events_set && g_events.on_touch_down && e.type == MEOW_EV_TOUCH_DOWN)
g_events.on_touch_down(e.a, e.b);
if (g_events_set && g_events.on_touch_move && e.type == MEOW_EV_TOUCH_MOVE)
g_events.on_touch_move(e.a, e.b);
got = 1; /* 交互事件:不睡眠,保持跟手帧同步 */
} else if (e.type == MEOW_EV_SWIPE_PROGRESS) {
if (in_startup) continue; /* 启动窗口进度条静音 */
int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */
/* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。
* 全屏/收合下「左缘返回手势保留」,顶部动态提示条(meow_ui_swipe_hint)照常绘制
* (与 agent-108:不再以 meow_ui_sys_statusbar_active()(全屏/隐藏=0)开关,否则
* 全屏时滑条完全不显示)。条位 y=meow_ui_statusbar_height():非全屏=状态栏下沿
* (28)、全屏=顶部、收合=提示线下缘(MUI_SBAR_COLLAPSE_H),与提示线不重叠加——
* 全屏与非全屏共用同一条绘制/进度路径,只 hy 不同,无全屏特有的坐标/方向/最大值逻辑。
* APP 若注册 on_swipe_progress 仍可自绘(系统条照常画;x_start+dx==当前X,幂等)。 */
meow_ui_swipe_hint(xc);
if (g_events_set && g_events.on_swipe_progress)
g_events.on_swipe_progress(x0, dx);
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
} 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;
/* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
meow_ui_gesture_end(); /* §6.4:长按已触发,其抬手不再 TAP/SWIPE——顺手释放捕获 */
/* M7.0a 缺口#7:view 路由先于 §6.3 通用长按回调(页 view 可消费长按)。 */
if (meow_view_dispatch_event(MEOW_EV_LONG_PRESS, e.a, e.b)) continue;
if (g_long_press_cb) g_long_press_cb(e.a, e.b);
}
}
uint64_t now = now_ms();
if (g_events_set && g_events.on_tick) {
if (now - last_tick_ms >= 16) {
last_tick_ms = now;
g_events.on_tick();
}
}
/* §12.2 动画节拍器:推进所有活动动画(无动画时零开销)。挂既有 on_tick 机制,
* 不引入常驻高频轮询;动画 update 回调只 flush 其影响区(脏区联动)。 */
meow_anim_tick();
/* A-0 全局状态栏 1s 轻刷:电量/WiFi/音量 + 时钟。时钟每秒变 → 重画顶条并上屏,
* 即使 APP 空闲(未主动 flush)也保持时钟走动。
* 「同一个状态栏」闭环:仅系统进程(launcher)刷新顶条;App 进程不渲染状态栏,
* 此处不再 flush 顶条(避免把 launcher 叠画的系统顶条冲成 App 背景)。数据仍刷新
* (meow_ui_sys_refresh 无副作用),App 侧 inset/热区不受影响。 */
static uint64_t last_sys_ms = 0;
if (now - last_sys_ms >= 1000) {
last_sys_ms = now;
meow_ui_sys_refresh();
if (meow_ui_sys_render_enabled() && meow_ui_sys_statusbar_active())
meow_ui_flush_region(0, 0, meow_ui_width(), MUI_SBAR_H);
}
if (meow_ime_active()) { /* 软键盘覆盖层随帧绘制(30fps 节流,省 CPU) */
static uint64_t last_ime_ms = 0;
uint64_t n = now_ms();
if (n - last_ime_ms >= 33) {
last_ime_ms = n;
meow_ime_draw();
meow_ui_flush();
}
if (!got) usleep(16000);
} else if (!got) {
usleep(16000);
}
}
}
/* M2.71-IME-fix:供 meow_ui_ime.c 的阻塞 IME 循环周期性喂 App on_tick。
* 阻塞 IME 循环不经过 meow_ui_run,无法靠主循环喂 tick,故在此开独立入口;
* 与 meow_ui_run 共用 last_tick_ms(二者互斥运行,无竞争)。调用方在 meow_ime_draw 之前调用,
* 使 App 重绘先进 back_buf、IME 面板随后覆盖(IME 始终最上,不被 App 重绘盖住)。 */
void meow_ui_feed_tick(void)
{
if (!(g_events_set && g_events.on_tick)) return;
uint64_t now = now_ms();
if (now - last_tick_ms < 16) return;
last_tick_ms = now;
g_events.on_tick();
}