Files
MeowOS-SDK/meow_ui_ime.c
T

640 lines
29 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;
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<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;
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;
}