/* wintest.c —— **开窗参数 / 屏幕信息** 试验台
 * wintest.c — a test bench for **window parameters / screen info**
 *
 * 这个程序只做一件事：把「**你开窗时声明了什么**」与「**宿主实际给了什么**」并排摆出来，
 * 让两边对不上的地方**一眼可见**。它是 E141 那条「开窗前问尺寸必然偏」的现场取证工具 ——
 * 那条规则的正解是「开窗传 0（尺寸交给宿主）、开完再问」，而**对不对只能量**：
 * 本程序把 (0,0,宽,高) 的边框和中心十字**画出来**，边框贴不上画布边就是坐标系不对。
 * This program does one thing: put "**what you declared when opening the window**" side by side with
 * "**what the host actually gave you**", so a mismatch is **visible at a glance**. It is the on-site
 * instrument for E141 ("asking for the size before opening is always off"); the sanctioned workaround is
 * "pass 0 when opening (let the host size it), ask afterwards" — and whether that is right **can only be
 * measured**: this program **draws** the (0,0,w,h) border and a centre crosshair, so if the border does not
 * hug the canvas edge the coordinate space is wrong.
 *
 * ── 操作（屏幕手柄 / 物理键盘同一套 Win32 虚拟键值）──
 * ── Controls (the on-screen gamepad and a physical keyboard use the same Win32 virtual key codes) ──
 *     G  手柄档（ui_win_open_ex + 要手柄）
 *     G  gamepad chrome (ui_win_open_ex + needs gamepad)
 *     K  键盘档（ui_win_open_pc + 要屏幕键盘）
 *     K  keyboard chrome (ui_win_open_pc + needs on-screen keyboard)
 *     N  无 chrome（画布吃满整屏）
 *     N  no chrome (the canvas fills the whole screen)
 *     L  循环 锁定方向：自动 → 纵向 → 横向 → 自动
 *     L  cycle the orientation lock: auto -> portrait -> landscape -> auto
 *     Q / 返回箭头  退出
 *     Q / Back arrow  quit
 *
 * ⚠ **宿主只有三档键盘**：手柄 / 屏幕键盘 / 都不要。没有独立的 `keypad` / `numpad` 档
 *   —— 键盘档在横屏时是**左右分栏的 PC 键盘**（见 `DrawWindowPage.xaml` 的布局说明）。
 * ⚠ **The host has only three keyboard modes**: gamepad / on-screen keyboard / neither. There is no
 *   separate `keypad` / `numpad` mode — the keyboard mode is a **PC keyboard split left/right in landscape**
 *   (see the layout notes in `DrawWindowPage.xaml`).
 *
 * 跑：
 * Run:
 *     vml run examples/c/test/wintest.c
 */
#include <vml_ui.h>
#include <stdlib.h>

/* 键盘档 */
/* Keyboard modes */
#define KB_GAMEPAD  0
#define KB_KEYBOARD 1
#define KB_NONE     2

#define LOGN 5          /* 消息日志留几行 */
                        /* how many lines of message log to keep */

int  g_lang;
int  g_kb = KB_GAMEPAD;
int  g_lock = VML_LOCK_AUTO;
int  g_w;
int  g_h;
int  g_open_w;          /* 开窗后**立刻**问到的（可能还是估算值） */
int  g_open_h;          /* what was read **immediately** after opening (may still be an estimate) */
int  g_had_resize;      /* 开窗后第一条 WINDOWRESIZE 到了没有（拿它跟上面那对数比一次） */
                        /* whether the first WINDOWRESIZE after opening has arrived (compared once against the pair above) */
int  g_flash;           /* 最近一次「尺寸/方向变了」的时刻（0=还没有） */
                        /* timestamp of the last "size/orientation changed" (0 = none yet) */
char g_log[LOGN][56];
int  g_logn = 0;
char g_txt[192];        /* 一行拼好的文字 */
                        /* one assembled line of text */
char g_est[3][64];      /* 三行「估算」文字 —— **只在开窗/尺寸变化时重算** */
                        /* the three "estimate" lines - **recomputed only on open / size change** */

/* 追加字符串（自己数长度；不依赖库里的拼接语义） */
/* Append a string (counting the length myself; not relying on the library's concatenation semantics) */
void cat(char* d, char* s) {
    int n = 0;
    int i = 0;
    while (d[n] != 0) { n = n + 1; }
    while (s[i] != 0) { d[n] = s[i]; n = n + 1; i = i + 1; }
    d[n] = 0;
}

/* 十进制（含负号） */
/* Decimal (with a minus sign) */
void fmt(int v, char* out) {
    char tmp[16];
    int n = 0;
    int i = 0;
    int neg = 0;
    if (v < 0) { neg = 1; v = 0 - v; }
    if (v == 0) { tmp[n] = '0'; n = n + 1; }
    while (v > 0) { tmp[n] = (char)(48 + (v % 10)); n = n + 1; v = v / 10; }
    if (neg != 0) { out[i] = '-'; i = i + 1; }
    while (n > 0) { n = n - 1; out[i] = tmp[n]; i = i + 1; }
    out[i] = 0;
}

/* ── 当前按着的键 ──
 * 消息驱动（KEYDOWN 加 / KEYUP 删）+ **每帧拿 `ui_key_is_down` 对一次账**。
 * ⚠ 光靠消息会**漏**：手指划出按键范围、系统吃掉 CANCEL 都会让 KEYUP 永远不来
 *   （`vml_ui.h` 的 `ui_key_is_down` 注释点名了这件事），于是那个键会永远"按着"。
 * ── Keys currently held ──
 * Message-driven (add on KEYDOWN / remove on KEYUP) plus **a reconcile pass per frame using
 * `ui_key_is_down`**. ⚠ Messages alone **leak**: a finger sliding off a button or the system eating
 * CANCEL means KEYUP never arrives (the `ui_key_is_down` comment in `vml_ui.h` calls this out), so that
 * key would stay "held" forever. */
#define KEYMAX 6
int  g_keys[KEYMAX];
int  g_keyn = 0;
char g_kbuf[8];

void keys_add(int k) {
    int i = 0;
    while (i < g_keyn) {
        if (g_keys[i] == k) { return; }
        i = i + 1;
    }
    if (g_keyn < KEYMAX) { g_keys[g_keyn] = k; g_keyn = g_keyn + 1; }
}

void keys_del(int k) {
    int i = 0;
    int j = 0;
    while (i < g_keyn) {
        if (g_keys[i] != k) { g_keys[j] = g_keys[i]; j = j + 1; }
        i = i + 1;
    }
    g_keyn = j;
}

void keys_reconcile(void) {
    int i = 0;
    int j = 0;
    while (i < g_keyn) {
        if (ui_key_is_down(g_keys[i]) != 0) { g_keys[j] = g_keys[i]; j = j + 1; }
        i = i + 1;
    }
    g_keyn = j;
}

/* 键码 → 名字（**键值一律取库里的 `VML_KEY_*`**，不在程序里重写一遍魔法数字；
 * 字母/数字用字符字面量表达，那本身就说清了是什么）。
 * ⚠ 手柄 **START 没有单独的常量** —— 它走的就是回车（`VML_KEY_ENTER`）。
 * Key code -> name (**key values come from the library's `VML_KEY_*`**; no magic numbers rewritten here;
 * letters/digits use character literals, which already say what they are).
 * ⚠ The gamepad **START has no constant of its own** - it goes through Enter (`VML_KEY_ENTER`). */
char* keyname(int k) {
    char b[8];
    int i = 0;
    if (k == VML_KEY_BACKSPACE) { return "Bksp"; }
    if (k == VML_KEY_ENTER)     { return "START"; }
    if (k == VML_KEY_SELECT)    { return "SELECT"; }
    if (k == VML_KEY_PAUSE)     { return "PAUSE"; }
    if (k == VML_KEY_ESCAPE)    { return "Esc"; }
    if (k == VML_KEY_SPACE)     { return "Space"; }
    if (k == VML_KEY_LEFT)      { return "<-"; }
    if (k == VML_KEY_UP)        { return "^"; }
    if (k == VML_KEY_RIGHT)     { return "->"; }
    if (k == VML_KEY_DOWN)      { return "v"; }
    if (k == VML_KEY_PAD_A)     { return "A"; }
    if (k == VML_KEY_PAD_B)     { return "B"; }
    if (k == VML_KEY_PAD_X)     { return "X"; }
    if (k == VML_KEY_PAD_Y)     { return "Y"; }
    /* 字母 / 数字：本身就是可读的字符，不必再立常量 */
    /* letters / digits: they are readable characters already, no constants needed */
    if ((k >= '0' && k <= '9') || (k >= 'A' && k <= 'Z') || (k >= 'a' && k <= 'z')) {
        g_kbuf[0] = (char)k;
        g_kbuf[1] = 0;
        return g_kbuf;
    }
    /* 其余：给个 `#码`，总比什么都不显示强 */
    /* everything else: show `#code`, better than showing nothing at all */
    g_kbuf[0] = '#';
    fmt(k, b);
    i = 0;
    while (b[i] != 0 && i < 5) { g_kbuf[i + 1] = b[i]; i = i + 1; }
    g_kbuf[i + 1] = 0;
    return g_kbuf;
}

/* 往日志里加一条（环形，满了顶掉最老的） */
/* Add one entry to the log (ring buffer; when full the oldest is dropped) */
void log_add(char* s) {
    int i = 0;
    int n = 0;
    if (g_logn == LOGN) {
        /* 整体上移一行 */
        /* shift every row up by one */
        i = 1;
        while (i < LOGN) {
            n = 0;
            while (n < 56) { g_log[i - 1][n] = g_log[i][n]; n = n + 1; }
            i = i + 1;
        }
        g_logn = LOGN - 1;
    }
    i = 0;
    while (i < 55 && s[i] != 0) { g_log[g_logn][i] = s[i]; i = i + 1; }
    g_log[g_logn][i] = 0;
    g_logn = g_logn + 1;
}

/* 重算三行「估算」。
 * ⚠ **别每帧问**：`ui_scr_get_w_ex` 在宿主侧每次都要**写一张静态表**（记下"这个数是我们给的"，
 *   开窗时拿它判断"程序主张的尺寸是不是问了我们要的"）。每帧问 6 次 = 每秒 60 次写那张共享表，
 *   而它**没有加锁**、主线程开窗时也要读它 —— 纯属自找麻烦，而且这几个数在两次尺寸变化之间根本不变。
 * Recompute the three "estimate" lines.
 * ⚠ **Do not ask every frame**: `ui_scr_get_w_ex` **writes a static table** host-side on every call
 *   (it records "this number came from us"; the host reads it at open time to tell whether the size a
 *   program claims is one it asked us for). Asking 6 times per frame = 60 writes/sec to that shared,
 *   **unlocked** table which the main thread also reads when opening a window - asking for trouble,
 *   and these numbers do not change between two size changes anyway. */
void est_refresh(void) {
    int i = 0;
    while (i < 3) {
        int chrome;
        int o = 0;
        char b[16];
        chrome = VML_CHROME_GAMEPAD;
        if (i == 1) { chrome = VML_CHROME_KEYBOARD; }
        if (i == 2) { chrome = VML_CHROME_NONE; }

        g_est[i][0] = 0;
        cat(g_est[i], g_lang == 0 ? "估算 " : "est ");
        if (i == 0) { cat(g_est[i], g_lang == 0 ? "手柄 " : "pad  "); }
        if (i == 1) { cat(g_est[i], g_lang == 0 ? "键盘 " : "kbd  "); }
        if (i == 2) { cat(g_est[i], g_lang == 0 ? "无   " : "none "); }

        while (o < 2) {
            cat(g_est[i], o == 0 ? (g_lang == 0 ? "竖 " : "P ") : (g_lang == 0 ? "横 " : "L "));
            fmt(ui_scr_get_w_ex(o, chrome), b);
            cat(g_est[i], b);
            cat(g_est[i], "x");
            fmt(ui_scr_get_h_ex(o, chrome), b);
            cat(g_est[i], b);
            cat(g_est[i], "  ");
            o = o + 1;
        }
        i = i + 1;
    }
}

/* 按当前档位开窗（**可重开** —— 换键盘档、换方向都要重新声明） */
/* Open the window for the current mode (**reopenable** - switching keyboard mode or orientation re-declares it) */
void reopen_window(void) {
    char* title;
    title = g_lang == 0 ? "开窗试验台" : "WinTest";
    if (g_kb == KB_KEYBOARD) {
        /* 电脑屏窗口：坐标空间固定（0 会兜底成 640x480），输入是键盘 + 鼠标 */
        /* PC-screen window: fixed coordinate space (0 falls back to 640x480), input is keyboard + mouse */
        ui_win_open_pc(title, 0, 0, VML_WIN_ROTATABLE, VML_WIN_NEED_KEYBOARD);
    } else {
        ui_win_open_ex(title, 0, 0, VML_WIN_ROTATABLE,
                       g_kb == KB_GAMEPAD ? VML_WIN_NEED_GAMEPAD : VML_WIN_NO_GAMEPAD);
    }
    ui_scr_set_keep_on(1);
    /* ⚠ **开窗之后**才问尺寸：这时给的是实测画布；开窗前问的只是估算（E141） */
    /* ⚠ Ask for the size **after** opening: this is the measured canvas; before the open it is only an estimate (E141) */
    g_w = ui_scr_get_w();
    g_h = ui_scr_get_h();
    if (g_w <= 0) { g_w = 360; }
    if (g_h <= 0) { g_h = 620; }
    g_open_w = g_w;
    g_open_h = g_h;
    g_had_resize = 0;
    est_refresh();
}

void draw(void) {
    int fs;
    int lh;
    int i;
    int slot;
    int n;
    int needle;          /* 这一帧光标画到第几行 */
                         /* which row the cursor is drawing on this frame */
    int border;

    ui_2d_clear(VML_COLOR_BLACK);

    fs = 12;
    if (g_w < 460) { fs = 10; }
    if (g_w >= 760) { fs = 15; }
    lh = fs + 6;
    needle = 6;

    /* ── 自检图形：外框 + 九宫格 + 两条对角线 ──
     * 三层都要**贴到画布四边**。任何一层贴不上（或只贴一半），就说明「宿主报的位置」与
     * 「渲染映到的矩形」不是同一个空间 —— 这正是 E141 那条的现场判据。
     * 对角线交于正中；九宫格的四个交点落在 (w/3, h/3) 一类的整数分位上，边界对不上时最扎眼。
     * ── Self-check shapes: outer border + 3x3 grid + both diagonals ──
     * All three must **reach the four canvas edges**. If any of them does not (or only half does), then
     * "the position the host reports" and "the rectangle the render maps to" are not the same space -
     * that is the on-site criterion for E141. The diagonals cross at the exact centre, and the grid's four
     * intersections land on integer thirds (w/3, h/3), which makes a mismatched boundary most obvious. */
    border = VML_COLOR_GRAY;
    if (g_flash != 0 && ui_time_get_ms() - g_flash < 1200) { border = VML_COLOR_YELLOW; }
    /* 外框 */
    /* outer border */
    ui_2d_rect(1, 1, g_w - 2, g_h - 2, border, 0, 2, 0);
    /* 九宫格 */
    /* 3x3 grid */
    ui_2d_line(g_w / 3, 2, g_w / 3, g_h - 3, VML_COLOR_NAVY, 1);
    ui_2d_line(g_w * 2 / 3, 2, g_w * 2 / 3, g_h - 3, VML_COLOR_NAVY, 1);
    ui_2d_line(2, g_h / 3, g_w - 3, g_h / 3, VML_COLOR_NAVY, 1);
    ui_2d_line(2, g_h * 2 / 3, g_w - 3, g_h * 2 / 3, VML_COLOR_NAVY, 1);
    /* 两条对角线 */
    /* both diagonals */
    ui_2d_line(2, 2, g_w - 3, g_h - 3, VML_COLOR_TEAL, 1);
    ui_2d_line(g_w - 3, 2, 2, g_h - 3, VML_COLOR_TEAL, 1);

    /* ── 1. 实测画布 ── */
    g_txt[0] = 0;
    cat(g_txt, g_lang == 0 ? "画布(实测) " : "canvas(measured) ");
    {
        char b[16];
        fmt(g_w, b);
        cat(g_txt, b);
        cat(g_txt, " x ");
        fmt(g_h, b);
        cat(g_txt, b);
    }
    ui_2d_text(8, needle, g_txt, VML_COLOR_WHITE, fs + 3, VML_ANCHOR_LEFT);
    needle = needle + lh + 4;

    /* ── 2. 三种 chrome × 两个方向的**估算**（开窗前问到的就是这些；数值由 `est_refresh` 备好） ── */
    i = 0;
    while (i < 3) {
        ui_2d_text(8, needle, g_est[i], VML_COLOR_SILVER, fs, VML_ANCHOR_LEFT);
        needle = needle + lh;
        i = i + 1;
    }
    needle = needle + 4;

    /* ── 3. 当前方向 / 语言 / 键盘 / 锁定 ── */
    g_txt[0] = 0;
    cat(g_txt, g_lang == 0 ? "方向 " : "orient ");
    cat(g_txt, ui_scr_get_orientation() == VML_ORIENT_LANDSCAPE
                  ? (g_lang == 0 ? "横向" : "landscape")
                  : (g_lang == 0 ? "纵向" : "portrait"));
    cat(g_txt, "   ");
    cat(g_txt, g_lang == 0 ? "语言 " : "lang ");
    cat(g_txt, g_lang == 0 ? "中文" : "English");
    ui_2d_text(8, needle, g_txt, VML_COLOR_WHITE, fs, VML_ANCHOR_LEFT);
    needle = needle + lh;

    g_txt[0] = 0;
    cat(g_txt, g_lang == 0 ? "键盘 " : "keyboard ");
    if (g_kb == KB_GAMEPAD) { cat(g_txt, g_lang == 0 ? "手柄(gamepad)" : "gamepad"); }
    if (g_kb == KB_KEYBOARD) { cat(g_txt, g_lang == 0 ? "屏幕键盘(keyboard)" : "keyboard"); }
    if (g_kb == KB_NONE) { cat(g_txt, g_lang == 0 ? "无(none)" : "none"); }
    cat(g_txt, "   ");
    cat(g_txt, g_lang == 0 ? "锁定 " : "lock ");
    if (g_lock == VML_LOCK_PORTRAIT) { cat(g_txt, g_lang == 0 ? "纵向" : "portrait"); }
    if (g_lock == VML_LOCK_LANDSCAPE) { cat(g_txt, g_lang == 0 ? "横向" : "landscape"); }
    if (g_lock == VML_LOCK_AUTO) { cat(g_txt, g_lang == 0 ? "自动" : "auto"); }
    ui_2d_text(8, needle, g_txt, VML_COLOR_WHITE, fs, VML_ANCHOR_LEFT);
    needle = needle + lh;

    /* ── 当前按下的键（同时按几个就把几个名字都列出来） ── */
    keys_reconcile();
    g_txt[0] = 0;
    cat(g_txt, g_lang == 0 ? "按键 " : "keys ");
    if (g_keyn == 0) {
        cat(g_txt, g_lang == 0 ? "(无)" : "(none)");
    } else {
        i = 0;
        while (i < g_keyn) {
            if (i > 0) { cat(g_txt, " + "); }
            cat(g_txt, keyname(g_keys[i]));
            i = i + 1;
        }
    }
    ui_2d_text(8, needle, g_txt, VML_COLOR_CYAN, fs, VML_ANCHOR_LEFT);
    needle = needle + lh + 4;

    /* ── 4. 多点触摸：每个**按着的**槽位一个圈 + 坐标 ──
     * ⚠ `ui_touch_fetch(slot)` 的返回值是「**这个槽位号合法**」（0..9 恒真），
     *   不是「这个槽位有手指」—— 有没有按在 `ui_touch_is_down()` 里（实测：不判它会有 10 个幽灵点）。
     * ── Multi-touch: one circle + coordinates per **pressed** slot ──
     * ⚠ The return value of `ui_touch_fetch(slot)` means "**this slot number is valid**" (always true for 0..9),
     *   not "this slot has a finger" - whether it is pressed lives in `ui_touch_is_down()`
     *   (measured: without checking it you get 10 ghost points). */
    g_txt[0] = 0;
    cat(g_txt, g_lang == 0 ? "触摸 " : "touch ");
    n = 0;
    slot = 0;
    while (slot < 10) {
        ui_touch_fetch(slot);
        if (ui_touch_is_down() != 0) { n = n + 1; }
        slot = slot + 1;
    }
    {
        char b[16];
        fmt(n, b);
        cat(g_txt, b);
        cat(g_txt, g_lang == 0 ? " 点" : " down");
    }
    ui_2d_text(8, needle, g_txt, VML_COLOR_LIME, fs, VML_ANCHOR_LEFT);
    needle = needle + lh;

    slot = 0;
    while (slot < 10) {
        ui_touch_fetch(slot);
        if (ui_touch_is_down() != 0) {
            int tx;
            int ty;
            char b[16];
            int px;
            int py;
            tx = ui_touch_get_x();
            ty = ui_touch_get_y();

            px = tx;
            py = ty;
            if (px < 16) { px = 16; }
            if (py < 16) { py = 16; }
            if (px > g_w - 16) { px = g_w - 16; }
            if (py > g_h - 16) { py = g_h - 16; }
            ui_2d_circle(px, py, 22, VML_COLOR_ORANGE, 0, 3);

            g_txt[0] = 0;
            cat(g_txt, g_lang == 0 ? "手指" : "slot");
            fmt(slot, b);
            cat(g_txt, b);
            cat(g_txt, " (");
            fmt(tx, b);
            cat(g_txt, b);
            cat(g_txt, ",");
            fmt(ty, b);
            cat(g_txt, b);
            cat(g_txt, ")");
            ui_2d_text(8, needle, g_txt, VML_COLOR_GOLD, fs, VML_ANCHOR_LEFT);
            needle = needle + lh;
        }
        slot = slot + 1;
    }
    needle = needle + 4;

    /* ── 5. 消息日志（尺寸/方向变了会在这里留痕 + 边框闪一下） ── */
    ui_2d_text(8, needle, g_lang == 0 ? "消息：" : "messages:", VML_COLOR_GRAY, fs, VML_ANCHOR_LEFT);
    needle = needle + lh - 2;
    i = 0;
    while (i < g_logn) {
        ui_2d_text(16, needle, g_log[i], VML_COLOR_GRAY, fs, VML_ANCHOR_LEFT);
        needle = needle + lh - 2;
        i = i + 1;
    }

    /* ── 6. 热键提示钉在底部 ── */
    ui_2d_text(8, g_h - fs - 8,
               g_lang == 0 ? "G 手柄  K 键盘  N 无  L 锁方向  Q 退出"
                           : "G pad  K kbd  N none  L lock  Q quit",
               VML_COLOR_GRAY, fs, VML_ANCHOR_LEFT);

    ui_2d_present();
}

int main(void) {
    int msg[4];
    int t;
    int k;
    int want = -1;       /* 请求换档（-1 = 不换） */
                         /* requested mode change (-1 = none) */

    g_lang = ui_scr_get_language();
    reopen_window();
    log_add(g_lang == 0 ? "已开窗（手柄档）" : "opened (gamepad)");

    while (ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 100);
        if (t == VML_MSG_WINDOWCLOSE) { break; }

        if (t == VML_MSG_WINDOWORIENT) {
            char b[16];
            g_txt[0] = 0;
            cat(g_txt, g_lang == 0 ? "方向→" : "orient->");
            cat(g_txt, msg[1] == VML_ORIENT_LANDSCAPE ? (g_lang == 0 ? "横向" : "landscape")
                                                      : (g_lang == 0 ? "纵向" : "portrait"));
            log_add(g_txt);
            est_refresh();
            g_flash = ui_time_get_ms();
        }

        if (t == VML_MSG_WINDOWRESIZE) {
            char b[16];
            /* ⚠ 开窗后**第一条** resize 才是判据：拿"开窗那一刻问到的"跟"宿主真正给的"比。
             *   两者不等 ⇒ 开窗后立刻问仍然是估算值，**必须以这条消息为准重排**（E141 的正解）。 */
            /* ⚠ Only the **first** resize after the open is the criterion: compare "what was read at open time"
             *   with "what the host actually gives". If they differ, the value read right after the open was still
             *   an estimate and you **must relay out from this message** (the sanctioned fix for E141). */
            if (g_had_resize == 0) {
                g_had_resize = 1;
                if (g_open_w != msg[1] || g_open_h != msg[2]) {
                    g_txt[0] = 0;
                    cat(g_txt, g_lang == 0 ? "开窗问到的 " : "at open ");
                    fmt(g_open_w, b); cat(g_txt, b);
                    cat(g_txt, "x");
                    fmt(g_open_h, b); cat(g_txt, b);
                    cat(g_txt, g_lang == 0 ? " 是估算，实际 " : " was an estimate; real ");
                    fmt(msg[1], b); cat(g_txt, b);
                    cat(g_txt, "x");
                    fmt(msg[2], b); cat(g_txt, b);
                    log_add(g_txt);
                } else {
                    log_add(g_lang == 0 ? "开窗问到的 = 实际（一致）"
                                        : "at-open value = real (match)");
                }
            }
            g_txt[0] = 0;
            cat(g_txt, g_lang == 0 ? "尺寸→" : "resize->");
            fmt(msg[1], b); cat(g_txt, b);
            cat(g_txt, "x");
            fmt(msg[2], b); cat(g_txt, b);
            log_add(g_txt);
            /* 尺寸变了：**重取一次大小**（`ui_scr_get_w/h`）、按新尺寸**全量重画**
             * （外框 / 九宫格 / 对角线都是按 g_w×g_h 现算的，下一帧自然就是新的）。
             * ⚠ 两个来源（消息里的 A/B 与重取的值）**不一定相等** —— 不等就留一条痕。
             * ── Size changed: **re-query the size** (`ui_scr_get_w/h`) and **redraw everything** for it
             * (the border / grid / diagonals are all computed from g_w x g_h, so the next frame is already new).
             * ⚠ The two sources (A/B in the message and the re-queried value) **are not necessarily equal** -
             * leave a mark when they differ. */
            {
                int qw;
                int qh;
                qw = ui_scr_get_w();
                qh = ui_scr_get_h();
                if (qw != msg[1] || qh != msg[2]) {
                    g_txt[0] = 0;
                    cat(g_txt, g_lang == 0 ? "重取 " : "requery ");
                    fmt(qw, b); cat(g_txt, b);
                    cat(g_txt, "x");
                    fmt(qh, b); cat(g_txt, b);
                    cat(g_txt, g_lang == 0 ? " ≠ 消息" : " != message");
                    log_add(g_txt);
                }
                g_w = msg[1];
                g_h = msg[2];
                if (g_w <= 0) { g_w = qw; }
                if (g_h <= 0) { g_h = qh; }
                if (g_w <= 0) { g_w = 360; }
                if (g_h <= 0) { g_h = 620; }
            }
            est_refresh();
            g_flash = ui_time_get_ms();
        }

        if (t == VML_MSG_KEYUP) {
            keys_del(msg[1]);
        }

        if (t == VML_MSG_KEYDOWN) {
            k = msg[1];
            keys_add(k);
            if (k == VML_KEY_ESCAPE || k == 'Q' || k == 'q') { break; }
            if (k == 'G' || k == 'g') { want = KB_GAMEPAD; }
            if (k == 'K' || k == 'k') { want = KB_KEYBOARD; }
            if (k == 'N' || k == 'n') { want = KB_NONE; }
            if (k == 'L' || k == 'l') {
                g_lock = g_lock + 1;
                if (g_lock > VML_LOCK_AUTO) { g_lock = VML_LOCK_PORTRAIT; }
                ui_scr_lock_orientation(g_lock);
                log_add(g_lang == 0 ? "锁定已切换" : "lock cycled");
            }
        }

        /* 换档在**消息处理之外**做：重开窗会重置消息队列，在循环里换会踩到自己 */
        /* The mode switch happens **outside message handling**: reopening resets the queue, so switching inside the loop trips over itself */
        if (want >= 0) {
            g_kb = want;
            want = -1;
            reopen_window();
            ui_msg_clear();
            log_add(g_lang == 0 ? "已重开窗（换档）" : "reopened (mode switch)");
        }

        draw();
    }

    ui_scr_set_keep_on(0);
    return 0;
}
