subtree pull sdk 来自主仓 pub/sdk(主仓 commit 68ce46a 等)。新增: - 布局引擎 meow_ui_box / 动画基座 meow_ui_anim / 滚动物理 meow_ui_scroll - §6.4 事件分发形式化 / W_LISTVIEW 虚拟化多行 / meow_str 字符串资源 i18n - sb_icons 动作图标扩充 + topbar 位图图标 / meow_img_downscale - tools/ 图标生成脚本与 SVG 源 - 对应宿主 test_*.c 与 demo_box_scroll.c build_sdk.sh 冲突解析:保留公开仓自包含版(vendored meow_font + -Iinclude -I.), OBJS 手工并入 4 个新模块(meow_ui_box/anim/scroll/str)。保留 vendored meow_font.c/ meow_ipc.h/meow_font.h/meow_notif.h,剔除宿主测试二进制与编译产物。
640 lines
29 KiB
C
640 lines
29 KiB
C
/*
|
||
* 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 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;
|
||
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;
|
||
}
|