/* * 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 #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 } 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; /* ---- 布局 ---- */ 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_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:ESC(取消/关闭) + 空格 + 退格;单独一行回车(OK=只提交不关闭)。 * M2.71-bug7:修掉旧版「同格重叠的 BACK+ENTER」与重复回车。 * M2.71-IME-fix:回车不再关闭(与 ASCII 一致),关闭走 ESC/背景。 */ add_ch_row(y, "1234567890"); add_ch_row(y + GCOL, "-/.:*#+="); { int total = 10 * GW + 9 * GAP; int x = (480 - total) / 2; add_key(x, y + GCOL * 2, GW, GH, K_ESC, 'X'); /* 取消/关闭(丢弃) */ add_key(x + GCOL, y + GCOL * 2, (GW + GAP) * 7, GH, K_SPACE, ' '); /* 空格 */ add_key(x + GCOL * 8, y + GCOL * 2, (GW + GAP) * 2 - GAP, GH, K_BACK, '<'); /* 退格 */ } /* 回车(OK=只提交不关闭) */ { int total = 10 * GW + 9 * GAP; int x = (480 - total) / 2; add_key(x + GCOL * 4, y + GCOL * 3, 2 * GW + GAP, GH, K_ENTER, '>'); } } } /* ---- 编辑动作 ---- */ static int is_letter(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } static void apply_key(const struct kbkey *k) { 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; } } /* ---- 绘制 ---- */ void meow_ime_draw(void) { if (!g_ime_active) return; const meow_theme *th = meow_theme_get(); /* 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); /* 键 */ 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); } } } /* ---- 命中 ---- */ int meow_ime_handle_tap(int x, int y) { if (!g_ime_active || g_ime_done) return 0; if (y < KB_TOP) { g_ime_result = 0; g_ime_done = 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; } 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; 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 打字交互也重置息屏计时 */ if (e.type == MEOW_EV_TAP) meow_ime_handle_tap(e.a, e.b); } /* 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; }