/* * 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 #include #include #include #include /* 阻塞 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; 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 "空格"; case K_ENTER: return "确定"; case K_SWITCH: return "拼/英"; case K_SYMBOL: return "?123"; 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, "清", 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, "拼音", 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, "(无候选)", 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, "中", 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, "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, "OK", 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, "X", 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= 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= 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; 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(); } 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; }