← 都来码 / Dolaima← 例程索引c/app/gallery.c原文

c/app/gallery.c · 27327 字节 · 下载原文

/* gallery.c —— 常用小程序⑧:**拍照 / 相册**(第 4 层:`ui_*` 图形界面)
 * gallery.c -- Everyday utility #8: **camera / photo gallery** (layer 4: the `ui_*` graphical interface)
 *
 * 干什么:一屏两个按钮 ——「拍照」走 `ui_camera_capture`、「从相册选」走 `ui_camera_pick`。
 * What it does: two buttons -- "Take a photo" goes through `ui_camera_capture` and "Pick from the
 * 两者都往同一个缓冲里写**沙箱相对路径**并返回**字节数**(0 = 取消 / 没权限 / 这一端没有相机)。
 * gallery" through `ui_camera_pick`. Both write a **sandbox-relative path** into one buffer and
 * 拿到图就用 `ui_2d_image` 画出来(按图框**等比缩放**,不拉伸),图下面两行写**文件名**与**像素尺寸**。
 * return its **byte count** (0 = cancelled / no permission / this end has no camera). A picture that
 * 像素尺寸是**自己读 PNG 头部 `IHDR` 得来的**(见 `probeImageSize`)—— 拿不到就写「尺寸未知」。
 * comes back is drawn with `ui_2d_image`, scaled to fit the box while keeping the aspect ratio; two
 * lines underneath show the **file name** and the **pixel size**, read from the PNG header (unreadable
 * files fall back to "size unknown").
 *
 * ## ⚠ 手机上才有 —— 桌面(vmlcli)上「本机没有相机/相册」才是**正确输出**
 * ## ⚠ Phone-only -- on the desktop (vmlcli) "no camera or gallery on this device" is the **correct output**
 *
 * 桌面脚手架是**无头进程**:没有 GUI 可以弹相机 / 相册,`ui_camera_*` 一律返回 0。
 * The desktop scaffold is a **headless process**: there is no GUI to take a photo or open a gallery, so
 * 而 **0 不是错误** —— 取消、没权限、这一端没有该能力,三者都是 0,程序该照常继续。
 * `ui_camera_*` always returns 0. That **0 is not an error**: a cancel, a missing permission and a
 * 所以本文件的第一条分支不是"图画得对不对",而是"**这台机器根本没有相机**",界面上必须**明说**,
 * missing capability all look the same, and the program should carry on. So the first branch here is
 * 而不是留一块空白(空白会被当成"我刚才点歪了")。
 * not "is the picture right" but "**this machine has no camera at all**", and the screen must **say so**
 * instead of leaving an empty box (an empty box reads as "I must have missed the button").
 *
 * ## 桌面怎么看到"有相机"那一半:宿主自带的输入脚本注入,**不用改一行源码**
 * ## Seeing the camera branch on the desktop: the host's input-script injection, with **no source change**
 *
 *     printf '0 camera .resource/tetris.appicon.png\n300 keydown 13\n360 keyup 13\n' > in.txt
 *     vmlcli gallery.c --vml-home third_party/vml --input in.txt --timeout 40 --frame gallery.png
 *
 * 第一条把"相机返回的路径"喂给宿主(`CliVmlHost.InjectCamera`),后两条是**按一下「拍照」按钮**
 * The first line feeds the path the camera would return into the host (`CliVmlHost.InjectCamera`); the
 * (回车 13 = 那个按钮),于是注入的那张图就被真的画出来了 —— 换一张图就是另一张画面。
 * other two press the "Take a photo" button (virtual key 13 = Enter), so the injected picture really is
 * ⚠ 注入只影响**脚手架**:手机端那条真实现(FilePicker / MediaPicker + 权限)一个字节都不动。
 * drawn. The injection only affects the **scaffold**; the phone's real implementation is untouched.
 *
 * ## 踩到的坑(都是这一轮真撞上的,写在这里省得下一个人再撞)
 * ## Pitfalls (all hit for real while writing this file; recorded so the next person does not)
 *
 * ① ⚠ **`ui_2d_image` 的路径是宿主**按进程工作目录**解的,而程序自己的 `fopen` 是按**沙箱根**解的**
 * ① ⚠ **The host resolves the `ui_2d_image` path against the process working directory, while the
 *   (`#110` 走 `FileSystemRoot`,见 `VMLRuntime.Syscall.OS.cs`)。两者**不是同一套尺子** ——
 *   program's own `fopen` resolves against the sandbox root** (`#110` goes through `FileSystemRoot`).
 *   实测(2026-10-07):在仓库根跑同一个相对路径,图**画不出来**、而 `fopen` 照样成功;
 *   They are not the same ruler: measured on 2026-10-07, running from the repository root with one
 *   把工作目录换成源文件所在目录,两边就都对上了。所以本文件的跑法要求**进程工作目录 = 源文件目录**
 *   relative path made the picture silently disappear while `fopen` still succeeded; changing the
 *   (手机端两者天然一致:都是工作区根)。
 *   working directory to the source directory made both agree. Hence the run recipe keeps the process's
 *   working directory equal to the source directory (on the phone the two coincide by construction).
 * ② ⚠ **`fopen` 的第二个参数是 `int`,不是 `"r"`**(`Lib/shared/src/file.c` 的签名是
 * ② ⚠ **`fopen`'s second argument is an `int`, not `"r"`** (the signature in
 *   `fopen(const char *path, int mode)`,0 = 只读 / 1 = 只写 / 2 = 读写)。`stdio.h` 里
 *   `Lib/shared/src/file.c` is `fopen(const char *path, int mode)`: 0 = read, 1 = write, 2 = read/write).
 *   声明的却是 `FILE *fopen(const char *, const char *)` —— 照那个签名写 `fopen(p, "r")`,
 *   `stdio.h` declares `FILE *fopen(const char *, const char *)` instead, so writing `fopen(p, "r")`
 *   模式收到一个**指针值**,`#110` 直接回 `-2`(`INVALID_PARAMETER`)**且不报错**。
 *   hands the syscall a **pointer value** as the mode and `#110` answers `-2` (`INVALID_PARAMETER`)
 *   本文件**不 include `stdio.h`**,自己按真签名声明(也顺带避开 `fgets` —— 那个函数在库里
 *   with no message at all. This file therefore **does not include `stdio.h`** and declares the real
 *   **根本没有实现**,链上去会报未定义)。
 *   signatures itself (which also avoids `fgets`, which has **no implementation** in the library and
 *   would fail to link).
 * ③ ⚠ **文件级标量别用负值初值**(本仓刚归档的 E187):`static int x = -1;` 读到的是 **0**。
 * ③ ⚠ **Never give a file-scope scalar a negative initializer** (this repo's E187): `static int x = -1;`
 *   所以下面所有哨兵量一律用**正数**(0 = 没有/未知,1 = 有),别拿 -1 当"还没有"。
 *   reads back as **0**. Every sentinel below is therefore a **positive** number instead (0 = absent,
 *   1 = present); `-1` is never used to mean "not yet".
 * ④ ⚠ **"这一端没有"每拍都要重问一次**(与 `level.c` 坑 ② 同源):`--input` 的事件由另一个线程按
 * ④ ⚠ **"This end has it" must be re-asked every tick** (same as pitfall 2 in `level.c`): the `--input`
 *   时刻投递,程序可能在注入落地之前就点过按钮了。所以按钮**一直可点**,点一次问一次,
 *   events are delivered by another thread on a timer, and the button may be pressed before the
 *   "没有"只是这一次的结果,不是永久结论。
 *   injection lands. So the button stays live, each press asks again, and "absent" is the result of
 *   one attempt rather than a permanent verdict.
 *
 * 版面:尺寸一律从宿主问(`ui_scr_get_w()/_h()`),颜色是真彩 `0xAARRGGBB`,画完必须 `ui_2d_present()`。
 * Layout: every size is asked of the host (`ui_scr_get_w()/_h()`), colours are true colour
 * 循环**必须有界**(例程要能被自动验收):空闲 10 秒自己退出,`Esc`/`q` 也退出。
 * `0xAARRGGBB`, and the frame must end with `ui_2d_present()`. The loop **must be bounded** (samples have
 * 写真正的程序时把 `IDLE_EXIT_MS` 去掉即可。
 * to be automatically verifiable): it exits after 10 s of idling, and `Esc`/`q` quits too. Drop
 * `IDLE_EXIT_MS` when you write a real program.
 */

#include <vml_ui.h>
#include <stdlib.h>   /* itoa --- digits to text via the library */

#define IDLE_EXIT_MS 10000        /*  空闲 10 秒自己退出(验收用) */
                                  /* idle exit after 10 s (for automated checks) */

#define PATH_CAP 200              /*  宿主写回来的路径缓冲区(一个路径,不是一张图) */
                                  /* the buffer the host writes the path into (a path, not a picture) */

#define BTN_H 44                  /*  两个按钮的高度 */
                                  /* height of the two buttons */

/* ⚠ 自己声明 `fopen` 家族(**真签名是 `int mode`**,见文件头坑 ②)——
 * ⚠ Declare the `fopen` family here (the **real signature takes an `int mode`**, pitfall 2) --
 * 不 include `stdio.h` 就没人和这份声明打架。
 * with no `stdio.h` included, nothing can contradict these declarations.
 */
int fopen(const char *path, int mode);      /*  0 = 只读 */
                                            /* 0 = read only */
int fclose(int handle);
int fread(int handle, void *buf, int count);

/* 配色(真彩 AA RRGGBB)*/
/* Colours (true colour AA RRGGBB) */
#define C_BG      0xFF0E1218
#define C_BOX     0xFF161B24
#define C_RING    0xFF3C4658
#define C_TITLE   0xFFE8E8F0
#define C_DIM     0xFF8A94A8
#define C_ERR     0xFFFF8A80     /*  没有这项能力 */
                                 /* capability absent */
#define C_NAME    0xFF6FC3FF     /*  文件名 */
                                 /* the file name */
#define C_SIZE    0xFFFFD166     /*  像素尺寸 */
                                 /* the pixel size */
#define C_BTN     0xFF233046
#define C_BTNHOT  0xFF2F4060

/* ── 全局状态 ───────────────────────────────────────────────────────── */
/* -- Global state -- */
static int SW;                    /*  画布宽 */
                                 /* canvas width */
static int SH;                    /*  画布高 */
                                 /* canvas height */
static int g_pad;                 /*  页边距 */
                                 /* page margin */
static int lang;                  /*  界面语言:0 = 中文,1 = English */
                                 /* UI language: 0 = Chinese, 1 = English */

static char g_path[PATH_CAP];     /*  宿主写回来的路径(空串 = 还没有图)*/
                                 /* the path the host wrote back (empty = no picture yet) */
static int g_have;                /*  1 = 手上有一张能画的图 */
                                 /* 1 = we hold a picture we can draw */
static int g_imgW;                /*  图的像素宽(0 = 读不出来)*/
                                 /* the picture's pixel width (0 = unreadable) */
static int g_imgH;                /*  图的像素高(0 = 读不出来)*/
                                 /* the picture's pixel height (0 = unreadable) */
static int g_none;                /*  1 = 上一次要图时这一端**没有**相机/相册 */
                                 /* 1 = the last attempt hit an end **without** a camera or gallery */
static int g_asked;               /*  1 = 至少点过一次按钮(首屏提示靠它收起来)*/
                                 /* 1 = the button has been pressed at least once */

/* 版面(画与命中判定**必须用同一份**,见 `hitButton`)*/
/* Layout (drawing and hit testing **must share this one copy**, see `hitButton`) */
static int boxX;
static int boxY;
static int boxW;
static int boxH;                  /*  图框 */
                                 /* the picture box */
static int b1X;
static int b1Y;
static int b1W;
static int b1H;                   /*  「拍照」按钮 */
                                 /* the "Take a photo" button */
static int b2X;
static int b2Y;
static int b2W;
static int b2H;                   /*  「从相册选」按钮 */
                                 /* the "Pick from the gallery" button */
static int infoY;                 /*  图框下面两行文字(文件名 / 尺寸)*/
                                 /* the two lines under the box (name / size) */

/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */

/* 整数 → 十进制串(`ui_2d_text` 只收字符串;第 4 层没有 `print_int` 那种东西)。
 * Integer to a decimal string (`ui_2d_text` takes strings only; layer 4 has no `print_int`).
 * ⚠ 返回的是**同一个静态缓冲** —— 别在一个表达式里连用两次。
 * ⚠ It returns **one static buffer** -- never use it twice in one expression.
 */
static char g_i10[16];
static char *numstr(int v) {
    itoa(v, g_i10);
    return g_i10;
}

/* 大端 4 字节(PNG 头里所有长度都是大端)。用乘法而不是 `<< 24`:后者在最高位为 1 时
 * A big-endian 4-byte value (every length in a PNG header is big-endian). Multiplication rather
 * 会得到负数(C 的 int 是 32 位有符号),而宽高永远不该是负的。
 * than `<< 24`, which turns negative once the top bit is set (C's int is 32-bit signed).
 */
static int be32(char *b, int at) {
    return (b[at] & 0xFF) * 16777216 + (b[at + 1] & 0xFF) * 65536
         + (b[at + 2] & 0xFF) * 256 + (b[at + 3] & 0xFF);
}

/* 读 PNG 头里的像素尺寸。**只认 PNG**:别的格式头部各有各的读法,认不出就把宽高留 0
 * Read the pixel size out of the PNG header. **PNG only**: every other format has its own header
 * (界面显示「尺寸未知」)—— 宁可承认不知道,也不要猜一个数出来。
 * layout, and an unreadable file leaves both at 0 (the UI prints "size unknown") -- better to admit
 * we do not know than to invent a number.
 */
static void probeImageSize(void) {
    int h;
    int n;
    char hdr[32];
    g_imgW = 0;
    g_imgH = 0;
    if (g_path[0] == 0) return;
    h = fopen(g_path, 0);
    if (h < 0) return;                         /*  打不开(越界 / 不存在)就当读不出来 */
                                              /* cannot open (out of sandbox / missing) -> unknown */
    n = fread(h, hdr, 26);
    fclose(h);
    if (n < 26) return;
    if ((hdr[0] & 0xFF) != 0x89) return;       /*  魔数 89 50 4E 47 = "\x89PNG" */
    if ((hdr[1] & 0xFF) != 0x50) return;       /* magic 89 50 4E 47 is "\x89PNG" */
    if ((hdr[2] & 0xFF) != 0x4E) return;
    if ((hdr[3] & 0xFF) != 0x47) return;
    if ((hdr[12] & 0xFF) != 0x49) return;      /*  偏移 12 起的 4 字节是块名,必须 "IHDR" */
    if ((hdr[13] & 0xFF) != 0x48) return;      /* the 4 bytes at offset 12 are the chunk name ("IHDR") */
    if ((hdr[14] & 0xFF) != 0x44) return;
    if ((hdr[15] & 0xFF) != 0x52) return;
    g_imgW = be32(hdr, 16);                    /*  宽在 16,高在 20(大端 4 字节)*/
                                              /* width at 16, height at 20 (big-endian, 4 bytes) */
    g_imgH = be32(hdr, 20);
}

/* 文件名(只要末段)—— 注入给的是路径,界面上写成整条会挤爆一行 */
/* The file name (last segment only) -- the injected value is a path, and printing it whole overflows the line */
static char g_base[64];
static char *baseName(void) {
    int i;
    int last;
    int n;
    last = -1;
    i = 0;
    while (g_path[i] != 0) {
        if (g_path[i] == '/') last = i;
        i = i + 1;
    }
    n = 0;
    i = last + 1;
    while (g_path[i] != 0 && n < 40) { g_base[n] = g_path[i]; i = i + 1; n = n + 1; }
    g_base[n] = 0;
    return g_base;
}

/* ── 拿图 ───────────────────────────────────────────────────────────── */
/* -- Getting a picture -- */

/* 要一张图。`fromGallery` 0 = 拍照,1 = 相册。
 * Ask for a picture. `fromGallery` 0 = take a photo, 1 = pick from the gallery.
 * ⚠ **0 不是错误**(取消 / 没权限 / 这一端没有相机,三者都是 0)——
 * ⚠ **0 is not an error** (a cancel, a missing permission and a missing camera all give 0) --
 * 这里把"没有"记下来由界面**明说**,而不是静默什么都不做。
 * so "absent" is recorded and the UI **says so**, instead of silently doing nothing.
 */
static void requestImage(int fromGallery) {
    int n;
    g_asked = 1;
    n = 0;
    if (fromGallery != 0) n = ui_camera_pick(g_path, PATH_CAP);
    else                  n = ui_camera_capture(g_path, PATH_CAP);
    if (n <= 0) {
        g_have = 0;
        g_none = 1;
        g_path[0] = 0;
        g_imgW = 0;
        g_imgH = 0;
        return;
    }
    g_none = 0;
    g_have = 1;
    /* 宿主保证末尾写了 0,但长度就是字节数,自己再补一次更稳(多余的字节不漏出去)。 */
    /* The host does terminate the string, but the length it returned is the byte count; writing the
     * terminator again is cheaper than trusting a buffer we do not own. */
    g_path[n] = 0;
    probeImageSize();
}

/* ── 版面 ───────────────────────────────────────────────────────────── */
/* -- Layout -- */

static void layout(void) {
    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 360;
    if (SH <= 0) SH = 620;

    g_pad = SW / 18;
    if (g_pad < 10) g_pad = 10;

    /* 两个按钮并排钉在底部,图框吃掉中间剩下的全部高度 —— 先算按钮,图框才有上界 */
    /* The two buttons sit side by side at the bottom and the box takes whatever height is left, so the
     * buttons are laid out first and the box is bounded by them. */
    b1H = BTN_H;
    b2H = BTN_H;
    b1Y = SH - g_pad - BTN_H;
    b2Y = b1Y;
    b1W = (SW - g_pad * 2 - 10) / 2;
    b2W = b1W;
    if (b1W < 60) b1W = 60;
    b2W = b1W;
    b1X = g_pad;
    b2X = SW - g_pad - b2W;

    /* 图框下面两行(文件名 / 尺寸):行高跟着字号走,两行都钉在按钮之上 */
    /* Two lines under the box (name / size): the row height follows the font, and both sit above the buttons */
    infoY = b1Y - 44;

    boxX = g_pad;
    boxW = SW - g_pad * 2;
    boxY = g_pad + 42;                    /*  标题两行之下 */
                                          /* below the two heading lines */
    boxH = infoY - boxY - 10;
    if (boxH < 80) boxH = 80;
}

/* ── 绘制 ───────────────────────────────────────────────────────────── */
/* -- Drawing -- */

/* 把图按**等比缩放**摆进图框(宽高各让一步,取更小的那个倍率 ⇒ 不拉伸也不出血)。
 * Fit the picture into the box while **keeping the aspect ratio** (both dimensions get a vote and the
 * 拿不到像素尺寸时退回"铺满图框":那至少能看见图片本身。
 * smaller ratio wins, so nothing is stretched or cropped). With an unknown size it falls back to
 * filling the box, which at least shows the picture.
 */
static void drawPicture(void) {
    int dw;
    int dh;
    int x;
    int y;
    int availW;
    int availH;
    dw = g_imgW;
    dh = g_imgH;
    availW = boxW - 16;
    availH = boxH - 16;
    if (availW < 8) availW = 8;
    if (availH < 8) availH = 8;
    if (dw <= 0 || dh <= 0) {
        dw = availW;
        dh = availH;
    } else if (dw * availH > dh * availW) {
        /* ⚠ 先乘后除:`dw * availH / dh` 写成 `dw / dh * availH` 会先整除成 0(整数除法)*/
        /* ⚠ Multiply before dividing: `dw / dh * availH` truncates to 0 first (integer division) */
        dh = dh * availW / dw;
        dw = availW;
    } else {
        dw = dw * availH / dh;
        dh = availH;
    }
    if (dw < 1) dw = 1;
    if (dh < 1) dh = 1;
    x = boxX + (boxW - dw) / 2;
    y = boxY + (boxH - dh) / 2;
    ui_2d_image(x, y, g_path, dw, dh);
}

static void drawBox(void) {
    int cy;
    ui_2d_rect(boxX, boxY, boxW, boxH, C_BOX, 1, 0, 8);
    ui_2d_rect(boxX, boxY, boxW, boxH, C_RING, 0, 1, 8);
    cy = boxY + boxH / 2;

    if (g_have != 0) {
        drawPicture();
        return;
    }
    if (g_none != 0) {
        /* ⚠ **降级分支**:明说"这台机器没有",别留一块空白。 */
        /* ⚠ **The degraded branch**: say plainly that this machine lacks it instead of showing a blank box. */
        ui_2d_text(SW / 2, cy - 24, lang == 0 ? "本机没有相机 / 相册" : "no camera or gallery here",
                   C_ERR, 17, VML_ANCHOR_CENTER);
        ui_2d_text(SW / 2, cy + 2, lang == 0 ? "(桌面上就是这样,手机上有)" : "(that is the desktop; phones have both)",
                   C_ERR, 12, VML_ANCHOR_CENTER);
        ui_2d_text(SW / 2, cy + 26, lang == 0 ? "再点一次也还是 0:这一端没有这个能力" : "pressing again still gives 0: this end lacks the capability",
                   C_DIM, 11, VML_ANCHOR_CENTER);
        return;
    }
    ui_2d_text(SW / 2, cy - 20, lang == 0 ? "点下面的按钮要一张图" : "press a button below to get a picture",
               C_DIM, 15, VML_ANCHOR_CENTER);
    ui_2d_text(SW / 2, cy + 4, lang == 0 ? "「拍照」或「从相册选」" : "\"Take a photo\" or \"Pick from the gallery\"",
               C_DIM, 12, VML_ANCHOR_CENTER);
}

static void drawInfo(void) {
    char buf[96];
    if (g_have == 0) return;
    buf[0] = 0;
    strcat(buf, lang == 0 ? "文件:" : "file: ");
    strcat(buf, baseName());
    ui_2d_text(SW / 2, infoY, buf, C_NAME, 13, VML_ANCHOR_CENTER);

    buf[0] = 0;
    if (g_imgW > 0 && g_imgH > 0) {
        strcat(buf, numstr(g_imgW));
        strcat(buf, " x ");
        strcat(buf, numstr(g_imgH));
        strcat(buf, lang == 0 ? " 像素" : " px");
    } else {
        strcat(buf, lang == 0 ? "尺寸未知(不是 PNG,或读不到文件)" : "size unknown (not a PNG, or unreadable)");
    }
    ui_2d_text(SW / 2, infoY + 20, buf, C_SIZE, 12, VML_ANCHOR_CENTER);
}

static void drawButton(int x, int y, int w, int h, char *label, char *key) {
    char buf[64];
    ui_2d_rect(x, y, w, h, C_BTN, 1, 0, h / 2);
    ui_2d_rect(x, y, w, h, C_BTNHOT, 0, 1, h / 2);
    buf[0] = 0;
    strcat(buf, label);
    strcat(buf, "  ");
    strcat(buf, key);
    ui_2d_text(x + w / 2, y + (h - 15) / 2, buf, C_TITLE, 15, VML_ANCHOR_CENTER);
}

static void drawButtons(void) {
    drawButton(b1X, b1Y, b1W, b1H, lang == 0 ? "拍照" : "Photo", "Enter");
    drawButton(b2X, b2Y, b2W, b2H, lang == 0 ? "从相册选" : "Gallery", "P");
}

static void draw(void) {
    ui_2d_clear(C_BG);

    ui_2d_text_styled(SW / 2, g_pad, lang == 0 ? "相机 / 相册" : "Camera / Gallery",
                      C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD);
    if (g_have != 0) {
        ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "ui_2d_image 按画布等比缩放" : "ui_2d_image, scaled to fit the box",
                   C_DIM, 11, VML_ANCHOR_CENTER);
    } else if (g_none != 0) {
        ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "这一端没有相机(返回 0,照常继续)" : "this end has no camera (0, carry on)",
                   C_ERR, 11, VML_ANCHOR_CENTER);
    } else {
        ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "手机上才有;桌面上会如实报「没有」" : "phones only; the desktop reports \"absent\"",
                   C_DIM, 11, VML_ANCHOR_CENTER);
    }

    drawBox();
    drawInfo();
    drawButtons();

    ui_2d_text(SW / 2, SH - 14, lang == 0 ? "「拍照」回车 / 「相册」P   Esc / q 退出"
                                          : "Enter = photo, P = gallery   Esc / q quits",
               C_DIM, 11, VML_ANCHOR_CENTER);

    ui_2d_present();      /* ⚠ 不调这句 = 黑屏 */
                          /* without this call you get a black screen */
}

/* ── 命中判定 ───────────────────────────────────────────────────────── */
/* -- Hit testing -- */

/* 点在哪个按钮里:0 = 没点着,1 = 拍照,2 = 相册(与 `drawButtons` 用的是**同一组**坐标)*/
/* Which button a tap landed on: 0 = none, 1 = photo, 2 = gallery (the **same** geometry `drawButtons` uses) */
static int hitButton(int px, int py) {
    if (px >= b1X && px < b1X + b1W && py >= b1Y && py < b1Y + b1H) return 1;
    if (px >= b2X && px < b2X + b2W && py >= b2Y && py < b2Y + b2H) return 2;
    return 0;
}

/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
    int msg[4];
    int t;
    int t0;
    int done;
    int hit;

    lang = ui_scr_get_language();          /*  开局查一次,别每帧调 */
                                          /* queried once, not every frame */

    layout();
    ui_win_open_ex(lang == 0 ? "相机 / 相册" : "Camera / Gallery",
                   0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    layout();                              /*  开窗之后画布尺寸才是准的,再算一次版面 */
                                          /* the canvas size is only final after the window opens, so lay out again */
    draw();

    t0 = ui_time_get_ms();
    done = 0;
    while (!done && ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 200);
        if (t == VML_MSG_NONE) {
            if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) done = 1;
            continue;
        }
        t0 = ui_time_get_ms();
        if (t == VML_MSG_KEYDOWN) {
            int k = msg[1];
            if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
            else if (k == VML_KEY_ENTER || k == VML_KEY_SPACE) { requestImage(0); draw(); }
            else if (k == 'p' || k == 'P') { requestImage(1); draw(); }
            continue;
        }
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
            hit = hitButton(msg[1], msg[2]);
            if (hit == 1) { requestImage(0); draw(); }
            else if (hit == 2) { requestImage(1); draw(); }
            continue;
        }
        if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
            layout();
            draw();
            continue;
        }
        if (t == VML_MSG_WINDOWCLOSE) done = 1;
    }
    return 0;
}