Files
MeowOS-For-R3P2/patch_v2/sdk/meow_ui_ime.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00

660 lines
30 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_ime.c — 内置触屏软键盘(B1.6)。仅英文 QWERTY / 数字,ASCII。
*
* 阻塞:meow_ui_text_input(ctx) 弹出键盘,自绘自分发直到关闭(ESC / 点背景取消)。
* 回车(K_ENTER)只提交(mark g_ime_result=1)不关闭,用户可继续输入。
* 阻塞期间周期性喂 App on_tick(meow_ui_feed_tick),动态内容(进度条/时钟)不卡死。
* 非阻塞:meow_ui_text_input_async(ctx) 打开后返回;meow_ui_run 在 on_tick 间隙
* 随帧绘制并拦截触摸,app 用 meow_ime_active()/meow_ime_text() 轮询。
*
* 键盘覆盖屏幕底部约 300px;点按输入;键位 44×36(touch target 达标)。
* M2.71-IME-fix:字母键面存小写("qwertyuiop"…),shift 开→大写、关→小写;
* 数字/符号键不随 shift 转换。旧版键面存大写 → 无论 shift 开关恒输出大写(bug)。
*/
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "meow_pinyin.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
/* 阻塞 IME 循环周期性喂 App on_tick(实现在 meow_ui_input.c,读该 TU 的 g_events)。
* 让 IME 打开期间 App 的动态内容(进度条/时钟)继续刷新;调用在 meow_ime_draw 之前,
* 保证 App 重绘先进 back_buf、IME 面板随后盖上去(IME 始终最上,不被 App 重绘覆盖)。 */
extern void meow_ui_feed_tick(void);
#define IME_MAXKEYS 96
#define KB_TOP 420 /* 面板顶(预览行起点) */
#define GW 44 /* 键宽 */
#define GH 36 /* 键高 */
#define GAP 4 /* 键间距 */
#define GCOL 48 /* 列步进 = GW+GAP */
/* M2.71-bug7:键盘区背景快照/恢复。旧实现关闭后键盘像素留在屏底(残影)。此处打开前拍下
* 键盘区 [KB_TOP,H),关闭后回填 + 上屏 → 无残影。 */
static unsigned char *g_ime_cap = NULL;
static int g_ime_cap_line = 0, g_ime_cap_h = 0;
static void ime_capture(void)
{
unsigned char *bb = meow_ui_back_buf();
int line = meow_ui_back_buf_pitch();
int h = meow_ui_height() - KB_TOP;
if (!bb || line <= 0 || h <= 0) { g_ime_cap = NULL; return; }
size_t n = (size_t)line * (size_t)h;
if (!g_ime_cap || g_ime_cap_line != line || g_ime_cap_h != h) {
free(g_ime_cap);
g_ime_cap = (unsigned char *)malloc(n);
g_ime_cap_line = line; g_ime_cap_h = h;
}
if (g_ime_cap) memcpy(g_ime_cap, bb + (size_t)line * KB_TOP, n);
}
static void ime_restore(void)
{
unsigned char *bb = meow_ui_back_buf();
if (!g_ime_cap || !bb) return;
size_t n = (size_t)g_ime_cap_line * (size_t)g_ime_cap_h;
memcpy(bb + (size_t)g_ime_cap_line * KB_TOP, g_ime_cap, n);
meow_gfx_mark_dirty(0, KB_TOP, meow_ui_width(), g_ime_cap_h);
meow_ui_flush();
}
typedef enum { K_CH, K_SHIFT, K_SPACE, K_BACK, K_ENTER, K_ESC,
K_PY, K_SWITCH, K_SYMBOL, K_COMMIT, K_CLEAR } kkind;
struct kbkey { int x, y, w, h; kkind kind; char ch; };
static struct kbkey g_keys[IME_MAXKEYS];
static int g_nkeys = 0;
static int g_ime_active = 0;
static int g_ime_done = 0;
static int g_ime_result = 0;
static int g_shift = 0;
/* WiFi 设置页 bug3(2026-09-15):IME 打开瞬间 TAP 冷却窗。触发打开的那次点按,其「抬起
* TAP」(含 goodix 抬起长尾)可能由 SDK 输入线程在打开 drain 之后才读出并入队(子页自有
* tfd 与 SDK 输入线程独立读同一 evdev,节奏可滞后 ~20ms+)——阻塞循环把它当「点背景=取消」
* 瞬间关键盘(y<KB_TOP)或幻影按键(y≥KB_TOP)。窗口期内的 TAP 一律消费丢弃(镜像
* meow_ui_run 的 STARTUP_COOLDOWN_MS 500 先例,取 200 兼顾手感;正常打字第一键必在窗后)。
* 防御为双保险:drain 已复位触摸状态机(在途按压不再产 TAP),冷却窗再兜住 drain 前已入队
* 未及清/时序竞争残漏的 TAP。 */
#define IME_TAP_COOLDOWN_MS 200
static uint64_t g_ime_open_ms = 0; /* meow_ui_time_ms() 单调毫秒;0=未记录 */
static meow_ime_ctx g_ime_ctx; /* 拷贝(on_change 回调用) */
static meow_ime_ctx *g_ime_orig = NULL; /* 应用真实 ctx(buf 回填用) */
static char g_ime_buf[256];
static int g_maxlen = 255;
/* ---- 拼音模式(MEOW_IME_PINYIN)几何 + 状态(设计稿 §4.1/§5.2)----
* 面板纵向分割:组合条 420..456、候选栏 456..504、键区 504..684(4 行×48,下缘留 36px)。
* 组合串 g_py_buf 是 IME 内部态,不进字段(字段只含已上屏文本);候选每页 ≤6。 */
#define PY_COMBO_Y (KB_TOP) /* 420 — 组合条顶 */
#define PY_COMBO_H 36 /* 组合条高 */
#define PY_CAND_Y 456 /* 候选栏顶 */
#define PY_CAND_H 48 /* 候选栏高 */
#define PY_KEY_TOP 504 /* 键区首行顶 */
#define PY_PAGE 6 /* 每页候选数 */
#define PY_MAXCAND 32 /* py_predict 候选上限(g_py_all 容量) */
#define PY_PREV_X 6
#define PY_PREV_W 34
#define PY_NEXT_X 440
#define PY_NEXT_W 34
#define PY_CAND_X0 44 /* 候选起始 x */
#define PY_CAND_GAP 14 /* 候选间距 */
#define PY_CLEAR_X 388 /* 组合条「清」按钮 */
#define PY_CLEAR_W 34
#define PY_BADGE_X 426 /* 组合条「拼音」模式徽标 */
#define PY_BADGE_W 46
#define PY_BADGE_H 28
#define PY_BADGE_Y (PY_COMBO_Y + 4)
static char g_py_buf[64]; /* 拼音组合串(去声调小写,≤8 音节) */
static char g_py_all[PY_MAXCAND][64];/* 当前组合全部候选(UTF-8) */
static int g_py_nc; /* 候选总数 */
static int g_py_page; /* 翻页偏移(每页 ≤6) */
static int g_py_active; /* 组合态标志 */
static int g_ime_swipe_y = -1; /* 最近按下 Y(候选栏滑动翻页判定) */
/* ASCII/NUM 顶条右侧「中」切换热区(不占键位,英文零回归;GEOM 与 meow_ime_draw / handle_tap 共用)。 */
#define ASCII_MID_X 430
#define ASCII_MID_W 44
/* ---- 布局 ---- */
static void add_key(int x, int y, int w, int h, kkind k, char ch)
{
if (g_nkeys >= IME_MAXKEYS) return;
g_keys[g_nkeys].x = x; g_keys[g_nkeys].y = y;
g_keys[g_nkeys].w = w; g_keys[g_nkeys].h = h;
g_keys[g_nkeys].kind = k; g_keys[g_nkeys].ch = ch;
g_nkeys++;
}
/* 加入一行居中字符键;返回该行开始 x */
static void add_ch_row(int y, const char *chars)
{
int n = (int)strlen(chars);
int total = n * GW + (n - 1) * GAP;
int x = (480 - total) / 2;
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y, GW, GH, K_CH, chars[i]);
}
static void build_kb(meow_ime_mode mode)
{
g_nkeys = 0;
int y = KB_TOP + 50;
if (mode == MEOW_IME_PINYIN) {
/* 4 行文本键(拼音只用到字母;数字/符号走 [?123] 切到既有 NUM 布局):
* 行首 y=504,行 y=504/552/600/648,下缘 684。 */
y = PY_KEY_TOP;
add_ch_row(y, "qwertyuiop");
/* 第 2 行:9 字母 + [拼/英](切 MEOW_IME_ASCII,复用 SHIFT 槽位) */
{
const char *r = "asdfghjkl";
int n = (int)strlen(r);
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL, GW, GH, K_CH, r[i]);
add_key(x + 9 * GCOL, y + GCOL, GW, GH, K_SWITCH, '&');
}
/* 第 3 行:7 字母 + [←退格 3格] */
{
const char *r = "zxcvbnm";
int n = (int)strlen(r);
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL * 2, GW, GH, K_CH, r[i]);
add_key(x + 7 * GCOL, y + GCOL * 2, (GW + GAP) * 3, GH, K_BACK, '<');
}
/* 第 4 行:[?123] [空格 6格] [OK](OK=只提交不关闭,同英文) */
{
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
add_key(x, y + GCOL * 3, GW, GH, K_SYMBOL, '1');
add_key(x + GCOL, y + GCOL * 3, (GW + GAP) * 6, GH, K_SPACE, ' ');
add_key(x + GCOL * 8, y + GCOL * 3, 2 * GW + GAP, GH, K_ENTER, '>');
}
} else if (mode == MEOW_IME_ASCII) {
add_ch_row(y, "1234567890");
add_ch_row(y + GCOL, "qwertyuiop");
/* 第 3 行:9 字母 + SHIFT */
{
const char *r = "asdfghjkl";
int n = (int)strlen(r);
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL * 2, GW, GH, K_CH, r[i]);
add_key(x + 9 * GCOL, y + GCOL * 2, GW, GH, K_SHIFT, '^');
}
/* 第 4 行:7 字母 + BACKSPACE */
{
const char *r = "zxcvbnm";
int n = (int)strlen(r);
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
for (int i = 0; i < n; i++) add_key(x + i * GCOL, y + GCOL * 3, GW, GH, K_CH, r[i]);
add_key(x + 7 * GCOL, y + GCOL * 3, (GW + GAP) * 3, GH, K_BACK, '<');
}
/* 第 5 行:ESC + SPACE(宽) + ENTER */
{
int total = 10 * GW + 9 * GAP;
int x = (480 - total) / 2;
add_key(x, y + GCOL * 4, GW, GH, K_ESC, 'X');
add_key(x + GCOL, y + GCOL * 4, (GW + GAP) * 6, GH, K_SPACE, ' ');
add_key(x + GCOL * 8, y + GCOL * 4, 2 * GW + GAP, GH, K_ENTER, '>');
}
} else { /* MEOW_IME_NUM:居中九键数字盘(用户定案)——1-9 从上到下 3×3,0 垫底居中;
* 顶行保留常用符号;底行 ESC(取消/关闭) | 0 | 退格+OK(只提交不关闭)。
* M2.71-bug7:修掉旧版「同格重叠的 BACK+ENTER」与重复回车。
* M2.71-IME-fix:回车不再关闭(与 ASCII 一致),关闭走 ESC/背景。 */
add_ch_row(y, "-/.:*#+="); /* 符号行(8 键居中) */
{
/* 九键:3 列 × 100px 宽、列距 20,整体 340 宽居中(x0=70,190,310)。 */
const int NW = 100, NG = 20;
const int x0 = (480 - (3 * NW + 2 * NG)) / 2; /* = 70 */
static const char pad[3][4] = { "123", "456", "789" };
for (int r = 0; r < 3; r++)
for (int c = 0; c < 3; c++)
add_key(x0 + c * (NW + NG), y + GCOL * (1 + r), NW, GH, K_CH, pad[r][c]);
/* 底行:ESC(取消/关闭) | 0 | BACK + OK(只提交不关闭)。BACK/OK 各半格。 */
int yb = y + GCOL * 4;
add_key(x0, yb, NW, GH, K_ESC, 'X');
add_key(x0 + (NW + NG), yb, NW, GH, K_CH, '0');
int x3 = x0 + 2 * (NW + NG); /* 第 3 列左缘 */
add_key(x3, yb, (NW - GAP) / 2, GH, K_BACK, '<');
add_key(x3 + (NW - GAP) / 2 + GAP, yb, (NW - GAP) / 2, GH, K_ENTER, '>');
}
}
}
/* ---- 编辑动作 ---- */
static int is_letter(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); }
/* ---- 拼音编辑动作(MEOW_IME_PINYIN) ---- */
static int py_pages(void) { return (g_py_nc + PY_PAGE - 1) / PY_PAGE; }
static void py_refresh(void)
{
g_py_nc = py_predict(g_py_buf, g_py_all, PY_MAXCAND);
g_py_page = 0;
g_py_active = (g_py_buf[0] != 0);
}
static void py_input(char c)
{
int len = (int)strlen(g_py_buf);
if (len < (int)sizeof(g_py_buf) - 1) { g_py_buf[len] = c; g_py_buf[len + 1] = 0; }
py_refresh();
}
static void py_back_combo(void)
{
int len = (int)strlen(g_py_buf);
if (len > 0) g_py_buf[len - 1] = 0;
py_refresh();
}
static void py_clear(void)
{
g_py_buf[0] = 0;
py_refresh();
g_ime_swipe_y = -1;
}
/* 上屏词条到字段 + on_change + 清组合。maxlen 为字节预算(含 NUL)。 */
static void py_commit(const char *s)
{
int len = (int)strlen(g_ime_buf);
int sl = (s && *s) ? (int)strlen(s) : 0;
if (sl > 0 && len + sl < g_maxlen) {
memcpy(g_ime_buf + len, s, (size_t)sl + 1);
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
}
py_clear();
}
/* UTF-8 安全退格:从字段末回退一个 codepoint(1-4 字节) */
static void py_back_field(void)
{
int len = (int)strlen(g_ime_buf);
if (len <= 0) return;
int i = len - 1;
while (i > 0 && ((unsigned char)g_ime_buf[i] & 0xC0) == 0x80) i--; /* 回退过续字节,找到起始字节 */
g_ime_buf[i] = 0;
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
}
static void py_page_next(void) { int p = py_pages(); if (g_py_page < p - 1) g_py_page++; }
static void py_page_prev(void) { if (g_py_page > 0) g_py_page--; }
static void py_commit_first(void) { if (g_py_nc > 0) py_commit(g_py_all[0]); }
static void py_set_mode(meow_ime_mode m)
{
g_ime_ctx.mode = m;
py_clear(); /* 组合态未上屏内容丢弃(设计稿 §5.3:关闭/切换同语义) */
g_shift = 0;
build_kb(m);
}
static void apply_key_py(const struct kbkey *k)
{
switch (k->kind) {
case K_CH: py_input((char)tolower((unsigned char)k->ch)); break; /* 仅 a-z 字母进组合 */
case K_BACK:
if (g_py_buf[0]) py_back_combo(); /* 组合非空:删组合末字母 */
else py_back_field(); /* 组合空:删字段末汉字(UTF-8 安全) */
break;
case K_SPACE:
if (g_py_buf[0]) py_commit_first(); /* 组合非空:上屏首候选 */
else { /* 空:上屏一个空格 */
int len = (int)strlen(g_ime_buf);
if (len < g_maxlen - 1) { g_ime_buf[len] = ' '; g_ime_buf[len + 1] = 0;
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx); }
}
break;
case K_ENTER:
if (g_py_buf[0]) py_commit_first(); /* 上屏首候选 + 清组合(不关闭) */
else g_ime_result = 1; /* 组合空:只提交不关闭(走 ESC/背景) */
break;
case K_SWITCH: py_set_mode(MEOW_IME_ASCII); break; /* 拼→英 */
case K_SYMBOL: py_set_mode(MEOW_IME_NUM); break; /* →数字/符号 */
case K_ESC: g_ime_result = 0; g_ime_done = 1; break;
case K_COMMIT: py_commit_first(); break;
case K_CLEAR: py_clear(); break;
default: break;
}
}
static void apply_key(const struct kbkey *k)
{
if (g_ime_ctx.mode == MEOW_IME_PINYIN) { apply_key_py(k); return; }
switch (k->kind) {
case K_CH: {
/* M2.71-IME-fix:键面存小写(build_kb "qwertyuiop"…),shift 开→大写、关→小写;
* 数字/符号(非字母)不转换。旧版存大写 + else 分支恒返回大写 → 大小写不区分。 */
char c = k->ch;
if (g_shift && is_letter(c)) c = (char)toupper((unsigned char)c);
int len = (int)strlen(g_ime_buf);
if (len < g_maxlen - 1) {
g_ime_buf[len] = c; g_ime_buf[len + 1] = 0;
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
}
break;
}
case K_SHIFT: g_shift = !g_shift; break;
case K_SPACE: {
int len = (int)strlen(g_ime_buf);
if (len < g_maxlen - 1) { g_ime_buf[len] = ' '; g_ime_buf[len + 1] = 0;
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx); }
break;
}
case K_BACK: {
int len = (int)strlen(g_ime_buf);
if (len > 0) { g_ime_buf[len - 1] = 0;
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx); }
break;
}
case K_ENTER: g_ime_result = 1; break; /* M2.71-IME-fix:回车只提交(mark),不关键盘;关闭走 ESC/背景 */
case K_ESC: g_ime_result = 0; g_ime_done = 1; break;
default: break; /* K_PY/K_SWITCH/K_SYMBOL/K_COMMIT/K_CLEAR 仅拼音布局出现,英文分支不触 */
}
}
/* ---- 绘制 ---- */
/* 拼音键面标签(K_CH=字母小写;其余为该键的内置文字)。 */
static const char *py_key_label(const struct kbkey *k, char *tmp)
{
switch (k->kind) {
case K_CH: { char c = (char)tolower((unsigned char)k->ch); tmp[0] = c; tmp[1] = 0; return tmp; }
case K_BACK: return "⌫";
case K_SPACE: return meow_str("ime.space");
case K_ENTER: return meow_str("ime.enter");
case K_SWITCH: return meow_str("ime.switch");
case K_SYMBOL: return meow_str("ime.symbol");
default: return "";
}
}
/* 候选栏内第 idx(页内 0..5)候选的实际 x/宽;越界返回 0。绘制与命中共用,保证几何一致。 */
static int py_cand_rect(int idx, int *x, int *w)
{
int gi = g_py_page * PY_PAGE + idx;
if (gi >= g_py_nc) return 0;
int xp = PY_CAND_X0;
for (int i = 0; i < idx; i++) {
int pgi = g_py_page * PY_PAGE + i;
if (pgi >= g_py_nc) break;
xp += meow_ui_text_width(g_py_all[pgi], 20) + PY_CAND_GAP;
}
*x = xp;
*w = meow_ui_text_width(g_py_all[gi], 20);
return 1;
}
static void meow_ime_draw_py(const meow_theme *th)
{
const int th20 = meow_ui_text_height(20);
/* 面板整体 + 组合条 + 候选栏 */
meow_ui_fill_rect(0, KB_TOP, 480, 720 - KB_TOP, th->surfaceContainerHighest);
meow_ui_fill_rect(0, PY_COMBO_Y, 480, PY_COMBO_H, th->surfaceContainerHigh);
/* 组合条:left=组合串实时显示(含 '_' 光标);right=「清」+ 模式徽标「拼音」 */
char pybuf[72];
snprintf(pybuf, sizeof pybuf, "%s_", g_py_buf);
meow_ui_text(14, PY_COMBO_Y + 8, pybuf, th->font_body, th->onSurface);
meow_gfx_fill_round(PY_CLEAR_X, PY_COMBO_Y + 4, PY_CLEAR_W, PY_BADGE_H, 8, th->secondaryContainer);
meow_ui_text_center(PY_CLEAR_X + PY_CLEAR_W / 2, PY_COMBO_Y + 4 + (PY_BADGE_H - th20) / 2, meow_str("ime.clear"), 20, th->onSecondaryContainer);
meow_gfx_fill_round(PY_BADGE_X, PY_BADGE_Y, PY_BADGE_W, PY_BADGE_H, 8, th->primaryContainer);
meow_ui_text_center(PY_BADGE_X + PY_BADGE_W / 2, PY_BADGE_Y + (PY_BADGE_H - th20) / 2, meow_str("ime.pinyin_mode"), 20, th->onPrimaryContainer);
/* 候选栏:◀ ▶ 翻页 + 横排候选(高亮第 0 项) */
meow_ui_fill_rect(0, PY_CAND_Y, 480, PY_CAND_H, th->surfaceContainerHighest);
meow_gfx_fill_round(PY_PREV_X, PY_CAND_Y, PY_PREV_W, PY_CAND_H, 8, th->surfaceContainerLow);
meow_ui_text_center(PY_PREV_X + PY_PREV_W / 2, PY_CAND_Y + (PY_CAND_H - th20) / 2, "◀", 20, th->onSurface);
meow_gfx_fill_round(PY_NEXT_X, PY_CAND_Y, PY_NEXT_W, PY_CAND_H, 8, th->surfaceContainerLow);
meow_ui_text_center(PY_NEXT_X + PY_NEXT_W / 2, PY_CAND_Y + (PY_CAND_H - th20) / 2, "▶", 20, th->onSurface);
if (g_py_nc == 0) {
meow_ui_text(PY_CAND_X0, PY_CAND_Y + (PY_CAND_H - th20) / 2, meow_str("ime.no_candidate"), th->font_body, th->onSurfaceVariant);
} else {
int x = PY_CAND_X0;
for (int i = 0; i < PY_PAGE; i++) {
int gi = g_py_page * PY_PAGE + i;
if (gi >= g_py_nc) break;
const char *c = g_py_all[gi];
int w = meow_ui_text_width(c, 20);
int ty = PY_CAND_Y + (PY_CAND_H - th20) / 2;
if (i == 0) {
meow_gfx_fill_round(x - 6, PY_CAND_Y + 6, w + 12, PY_CAND_H - 12, 8, th->secondaryContainer);
meow_ui_text(x, ty, c, 20, th->onSecondaryContainer);
} else {
meow_ui_text(x, ty, c, 20, th->onSurface);
}
x += w + PY_CAND_GAP;
}
}
/* 4 行键 */
for (int i = 0; i < g_nkeys; i++) {
const struct kbkey *k = &g_keys[i];
uint32_t bg, fg;
if (k->kind == K_ENTER) { bg = th->primary; fg = th->onPrimary; }
else if (k->kind == K_SWITCH){ bg = th->tertiaryContainer; fg = th->onTertiaryContainer; }
else if (k->kind == K_CH) { bg = th->surfaceContainerLow; fg = th->onSurface; }
else { bg = th->primaryContainer; fg = th->onPrimaryContainer; }
meow_gfx_fill_round(k->x, k->y, k->w, k->h, 8, bg);
char tmp[4];
meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - th20) / 2, py_key_label(k, tmp), 20, fg);
}
}
void meow_ime_draw(void)
{
if (!g_ime_active) return;
const meow_theme *th = meow_theme_get();
if (g_ime_ctx.mode == MEOW_IME_PINYIN) { meow_ime_draw_py(th); return; }
/* M2.71-bug7:配色全走 meow_theme(深/浅主题自动切),不再硬编码黑底白键。 */
meow_ui_fill_rect(0, KB_TOP, 480, 720 - KB_TOP, th->surfaceContainerHighest); /* 面板 */
meow_ui_fill_rect(0, KB_TOP, 480, 42, th->surfaceContainerHigh); /* 回显条 */
char preview[260]; /* g_ime_buf(≤255) + '_' + NUL → 需 ≥257,防截断告警 */
/* 回显=用户已输入的字符(live echo,非候选/联想),随键入实时刷新。 */
snprintf(preview, sizeof preview, "%s_", g_ime_buf);
meow_ui_text(14, KB_TOP + 9, preview, th->font_body, th->onSurface);
/* ASCII/NUM 顶条右侧「中」热区:切到拼音模式(英文键布局零改动,零回归)。 */
{
int th20 = meow_ui_text_height(20);
meow_gfx_fill_round(ASCII_MID_X, KB_TOP + 7, ASCII_MID_W, 28, 8, th->primaryContainer);
meow_ui_text_center(ASCII_MID_X + ASCII_MID_W / 2, KB_TOP + 7 + (28 - th20) / 2, meow_str("ime.to_pinyin"), 20, th->onPrimaryContainer);
}
/* 键 */
for (int i = 0; i < g_nkeys; i++) {
const struct kbkey *k = &g_keys[i];
uint32_t bg, fg;
if (k->kind == K_ENTER) { bg = th->primary; fg = th->onPrimary; } /* OK=提交 */
else if (k->kind == K_CH) { bg = th->surfaceContainerLow; fg = th->onSurface; } /* 字符键 */
else { bg = th->primaryContainer; fg = th->onPrimaryContainer; } /* 功能键(ESC/SHIFT/空格/退格) */
meow_gfx_fill_round(k->x, k->y, k->w, k->h, 8, bg);
char label[2] = { k->ch, 0 };
if (is_letter(k->ch)) label[0] = g_shift ? (char)toupper((unsigned char)k->ch)
: (char)tolower((unsigned char)k->ch);
if (k->kind == K_SPACE) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.space"), th->font_body, fg); }
else if (k->kind == K_ENTER) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.enter"), 20, fg); }
else if (k->kind == K_ESC) { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, meow_str("ime.esc"), 20, fg); }
else { meow_ui_text_center(k->x + k->w / 2, k->y + (k->h - meow_ui_text_height(20)) / 2, label, 20, fg); }
}
}
/* ---- 命中 ---- */
/* 找命中键(供拼音长按判定退格用)。 */
static const struct kbkey *key_at(int x, int y)
{
for (int i = 0; i < g_nkeys; i++) {
const struct kbkey *k = &g_keys[i];
if (x >= k->x && x < k->x + k->w && y >= k->y && y < k->y + k->h) return k;
}
return NULL;
}
/* 拼音命中:候选栏/组合条/键区。y<KB_TOP=取消(组合不上屏)。 */
static int meow_ime_handle_tap_py(int x, int y)
{
if (y < KB_TOP) { g_ime_result = 0; g_ime_done = 1; return 1; } /* 背景点=取消 */
if (y >= PY_CAND_Y && y < PY_CAND_Y + PY_CAND_H) { /* 候选栏 */
if (x >= PY_PREV_X && x < PY_PREV_X + PY_PREV_W) { py_page_prev(); return 1; }
if (x >= PY_NEXT_X && x < PY_NEXT_X + PY_NEXT_W) { py_page_next(); return 1; }
/* 点候选:命中即上屏(UTF-8),清组合 */
for (int i = 0; i < PY_PAGE; i++) {
int cx, cw;
if (!py_cand_rect(i, &cx, &cw)) break;
if (x >= cx - 6 && x < cx + cw + 6) { py_commit(g_py_all[g_py_page * PY_PAGE + i]); return 1; }
}
return 1; /* 候选栏空隙:消费但不做 */
}
if (y >= PY_COMBO_Y && y < PY_COMBO_Y + PY_COMBO_H) { /* 组合条 */
if (x >= PY_CLEAR_X && x < PY_CLEAR_X + PY_CLEAR_W) { py_clear(); return 1; } /* 清组合 */
if (x >= PY_BADGE_X && x < PY_BADGE_X + PY_BADGE_W) { py_set_mode(MEOW_IME_ASCII); return 1; } /* 拼→英 */
return 1; /* 组合条其他:消费 */
}
const struct kbkey *k = key_at(x, y);
if (k) { apply_key(k); return 1; }
return 1; /* 键区/面板空隙:消费但不做 */
}
int meow_ime_handle_tap(int x, int y)
{
if (!g_ime_active || g_ime_done) return 0;
if (g_ime_ctx.mode == MEOW_IME_PINYIN) return meow_ime_handle_tap_py(x, y);
/* ASCII/NUM:顶条(回显条 KB_TOP..KB_TOP+42)右端「中」热区 → 切拼音;
* y<KB_TOP=上方背景=取消;顶条其它区域消费不取消(沿用既有行为)。 */
if (y < KB_TOP + 42) {
if (x >= ASCII_MID_X && x < ASCII_MID_X + ASCII_MID_W) { py_set_mode(MEOW_IME_PINYIN); return 1; }
if (y < KB_TOP) { g_ime_result = 0; g_ime_done = 1; return 1; } /* 背景点=取消 */
return 1; /* 顶条其它:消费但不做(不落键) */
}
for (int i = 0; i < g_nkeys; i++) {
const struct kbkey *k = &g_keys[i];
if (x >= k->x && x < k->x + k->w && y >= k->y && y < k->y + k->h) {
apply_key(k);
return 1;
}
}
return 1; /* 面板空隙:消费但不做 */
}
/* ---- 对外 ---- */
int meow_ime_active(void) { return g_ime_active && !g_ime_done; }
const char *meow_ime_text(void) { return g_ime_buf; }
const char *meow_ime_composition(void) { return g_ime_ctx.mode == MEOW_IME_PINYIN ? g_py_buf : ""; }
int meow_ime_mode_pinyin(void) { return g_ime_active && !g_ime_done && g_ime_ctx.mode == MEOW_IME_PINYIN; }
/* 候选栏横滑翻页:滑动起始于候选栏区(y∈[456,504))才消费;首/尾页钳位。 */
int meow_ime_swipe(int dir, int dist)
{
(void)dist;
if (!meow_ime_mode_pinyin()) return 0;
if (g_ime_swipe_y < PY_CAND_Y || g_ime_swipe_y >= PY_CAND_Y + PY_CAND_H) return 0;
if (dir == MEOW_SWIPE_LEFT) { py_page_next(); return 1; }
if (dir == MEOW_SWIPE_RIGHT) { py_page_prev(); return 1; }
return 0;
}
/* 拼音长按退格:长按落在「退格」键上 → 清空字段 + 组合(上屏 off 回调)。 */
int meow_ime_long_press(int x, int y)
{
if (!meow_ime_mode_pinyin()) return 0;
const struct kbkey *k = key_at(x, y);
if (!k || k->kind != K_BACK) return 0;
g_ime_buf[0] = 0;
py_clear();
if (g_ime_ctx.on_change) g_ime_ctx.on_change(g_ime_buf, g_ime_ctx.ctx);
return 1;
}
/* 统一事件入口:TAP/TOUCH_DOWN/SWIPE/LONG_PRESS(IME 打开时主循环/阻塞循环调用)。
* ASCII/NUM:TAP 走既有 handle_tap,其余返回 0(沿用英文行为,零回归)。 */
int meow_ime_handle_event(const meow_input_ev *e)
{
if (!g_ime_active || g_ime_done) return 0;
/* 打开冷却窗(bug3):窗口内 TAP 一律消费丢弃——防触发打开那次点按的迟到「抬起 TAP」
* 被当「点背景=取消」或幻影按键。消费(return 1)而非透传:事件已属 IME 会话期。 */
if (e->type == MEOW_EV_TAP && g_ime_open_ms &&
meow_ui_time_ms() - g_ime_open_ms < IME_TAP_COOLDOWN_MS)
return 1;
switch (e->type) {
case MEOW_EV_TAP: return meow_ime_handle_tap(e->a, e->b);
case MEOW_EV_TOUCH_DOWN: g_ime_swipe_y = e->b; return 1; /* 记录压点(候选栏滑动判定);覆盖层消费 */
case MEOW_EV_SWIPE: return meow_ime_swipe(e->a, e->b);
case MEOW_EV_LONG_PRESS: return meow_ime_long_press(e->a, e->b);
default: return 0;
}
}
void meow_ime_close(int result)
{
if (!g_ime_active) return;
if (result && g_ime_orig) {
strncpy(g_ime_orig->buf, g_ime_buf, sizeof g_ime_orig->buf - 1);
g_ime_orig->buf[sizeof g_ime_orig->buf - 1] = 0;
}
g_ime_active = 0; g_ime_done = 0;
ime_restore(); /* M2.71-bug7:关闭还原键盘区背景,无残影 */
}
void meow_ui_text_input_async(meow_ime_ctx *ctx)
{
if (!ctx) return;
g_ime_orig = ctx;
g_ime_ctx = *ctx; /* 拷贝(on_change 用) */
strncpy(g_ime_buf, ctx->buf, sizeof g_ime_buf - 1);
g_ime_buf[sizeof g_ime_buf - 1] = 0;
g_maxlen = ctx->maxlen > 0 ? ctx->maxlen : 255;
g_shift = 0; g_ime_result = 0; g_ime_done = 0; g_ime_active = 1;
/* 拼音状态复位(组合/候选/翻页/压点) */
g_py_buf[0] = 0; g_py_nc = 0; g_py_page = 0; g_py_active = 0; g_ime_swipe_y = -1;
build_kb(ctx->mode);
ime_capture(); /* M2.71-bug7:打开前拍下键盘区背景(关闭即还原) */
meow_input_start();
meow_input_drain();
/* bug3 加固(对齐 s_dd_modal/subpage 吸收 goodix 抬起长尾的「排空→40ms→再排空」惯例):
* 第一遍 drain 后等 40ms 让输入线程读出迟到事件,第二遍 drain 连状态机一并复位。 */
usleep(40000);
meow_input_drain();
g_ime_open_ms = meow_ui_time_ms(); /* TAP 冷却窗起点(见 IME_TAP_COOLDOWN_MS) */
}
int meow_ui_text_input(meow_ime_ctx *ctx)
{
if (!ctx) return 0;
meow_ui_text_input_async(ctx);
/* 阻塞循环:直到 done;on_change 由 handle_tap 触发 */
while (meow_ime_active()) {
/* flush 挂起:整个周期内(事件处理触发的 on_change 重绘 + App on_tick 重绘)
* 只记脏不上屏;IME 画完后一次 flush 原子上屏——否则 on_tick 的 invalidate
* 会先把进度条等区域刷上屏盖住键盘,IME 随后再盖回,同区一帧两刷→频闪。 */
meow_gfx_flush_hold(1);
meow_input_ev e;
while (meow_input_pop(&e)) {
meow_ui_activity_ping(); /* M2.71-bug5:IME 打字交互也重置息屏计时 */
meow_ime_handle_event(&e); /* TAP/TOUCH_DOWN/SWIPE/LONG_PRESS 统一分发(拼音/英文) */
}
/* 拼音 IME 打开时喂长按轮询(长按退格清空字段);英文模式不喂(保持既有行为)。 */
if (meow_ime_mode_pinyin()) meow_input_long_press_poll();
/* M2.71-IME-fix:阻塞 IME 期间喂 App on_tick(动态内容继续刷,progress/时钟不卡死)。
* 顺序要求:先喂 on_tick(App 重绘进 back_buf)→ 再 meow_ime_draw(IME 面板随后盖上去),
* 保证 IME 面板覆盖在最上,不被 App 的 d_paint_region 覆盖。 */
meow_ui_feed_tick();
meow_gfx_flush_hold(0);
meow_ime_draw();
meow_ui_flush();
usleep(16000);
}
int result = g_ime_result;
meow_ime_close(result);
return result;
}