← 都来码 / Dolaima← 例程索引cpp/ui/demo_ui.cpp原文

cpp/ui/demo_ui.cpp · 14259 字节 · 下载原文

// demo_ui.cpp —— **第 4 层:最新 UI 接口**(`ui_*` / `Lib/c/vml_ui.h`)

// demo_ui.cpp — **Layer 4: the newest UI interface** (`ui_*` / `Lib/c/vml_ui.h`)

//

// 与 `Examples/c/demo_ui.c` 同构:开 UI 窗口 → 查屏幕方向 → 按画布尺寸排版 → 画图元

// It has the same structure as `Examples/c/demo_ui.c`: open a UI window → query the screen orientation → lay out per canvas size → draw primitives

// → `ui_2d_present()` → 消息循环(触摸 / 按键 / 定时器 / 转屏)→ **有界退出**。

// → `ui_2d_present()` → message loop (touch / keys / timer / rotation) → **bounded exit**.

//

// ## 这一层与 BGI(第 3 层)的差别

// ## How this layer differs from BGI (Layer 3)

//

//   · **没有固定分辨率**:画布多大由宿主给(`ui_scr_get_w/h`),程序现排版。

//   · **No fixed resolution**: the host decides how big the canvas is (`ui_scr_get_w/h`), and the program lays out on the fly.

//   · **真彩**:`0xAARRGGBB`,不是 16 个索引色。

//   · **True color**: `0xAARRGGBB`, not 16 indexed colors.

//   · **有消息循环**:触摸/按键/定时器/窗口被关/被转屏都从 `ui_msg_wait` 出来。

//   · **It has a message loop**: touch/key/timer/window-closed/rotation all come out of `ui_msg_wait`.

//

// ## 为什么这一层不走 `graphics.h`

// ## Why this layer does not go through `graphics.h`

//

// `graphics.h` 是给**老程序**的兼容垫层(BGI 那套写死的坐标系)。新写的程序直接用

// `graphics.h` is a compatibility shim for **old programs** (BGI's hardcoded coordinate system). Newly written programs go

// `vml_ui.h` —— 尤其是"要把版面跟着屏幕尺寸排"的程序,BGI 那套固定坐标反而碍事。

// straight to `vml_ui.h` — especially programs that have to lay their layout out following the screen size, where BGI's fixed coordinates get in the way.

//

// ## ⚠ C++ 前端的两条坑(本 demo 的写法就是为绕开它们)

// ## ⚠ Two pitfalls of the C++ frontend (this demo is written the way it is to avoid them)

//

//   · `cout << <char* 变量>` 打的是**地址**(详见 `demo_std.cpp` 的文件头)⇒

//   · `cout << <char* variable>` prints the **address** (see the file header of `demo_std.cpp` for details) ⇒

//     这里要印字符串变量一律 `printf("%s", …)`。

//     here, printing a string variable always goes through `printf("%s", …)`.

//   · 类的方法**不生成函数体**(`未定义的函数 'method_…'`)⇒ 全部写成自由函数 +

//   · Class methods **do not get a generated body** (an "undefined function 'method_…'" error) ⇒ write everything as free functions +

//     一个文件级数组(与 `cpp/snake.cpp` 同一路子)。

//     one file-level array (the same approach as `cpp/snake.cpp`).

//

// 跑法:手机 `vml run examples/cpp/demo_ui.cpp`;

// How to run: on the phone `vml run examples/cpp/demo_ui.cpp`;

//       桌面 `vmlcli Examples/cpp/demo_ui.cpp --frames /tmp/fr`

//       on the desktop `vmlcli Examples/cpp/demo_ui.cpp --frames /tmp/fr`


#include <vml_ui.h>
#include <stdio.h>

#define MAX_FRAMES 60          // 到点自己停:60 拍 × 60ms ≈ 3.6 秒

// stops by itself when it is reached: 60 ticks × 60ms ≈ 3.6 seconds


// 版面变量:每次重排都按当前画布尺寸算(**不写死**)

// Layout variables: recomputed from the current canvas size on every re-layout (**never hardcoded**)

static int SW;
static int SH;
static int GY;

// 计数:证明这些事件真的到过

// Counters: proof that these events really did arrive

static int g_frames;
static int g_keys;
static int g_touches;
static int g_orient;

// 最近一次触摸的位置(-1 = 还没点过)

// Position of the most recent touch (-1 = not touched yet)

static int g_tx = -1;
static int g_ty = -1;

// 界面语言:开局查一次(`ui_scr_get_language` 是 syscall,别每帧调)

// UI language: queried once at startup (`ui_scr_get_language` is a syscall, do not call it every frame)

static int gLang;

// 数字 → 字符串。不用 std::string(C++ 前端整体不可用),也不用 sprintf(各前端形态不一),

// Number → string. No std::string (unusable across the C++ frontend), and no sprintf (its form differs per frontend),

// 自己按位拆最省事 —— 与仓库里其它语言的例程同一做法。

// so splitting the digits out yourself is the least hassle — the same approach as the examples in the other languages in this repo.

static char g_num[12];

static char *numstr(int v)
{
    int n = 0;
    int m = 0;
    char tmp[12];
    if (v < 0) { g_num[0] = '-'; v = -v; n = 1; }
    if (v == 0) { g_num[n] = '0'; g_num[n + 1] = 0; return g_num; }
    while (v > 0) { tmp[m] = (char)('0' + v % 10); v = v / 10; m = m + 1; }
    while (m > 0) { m = m - 1; g_num[n] = tmp[m]; n = n + 1; }
    g_num[n] = 0;
    return g_num;
}

static void relayout()
{
    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 360;
    if (SH <= 0) SH = 620;
    /* 内容整块**在画布内垂直居中**(用户 2026-10-07 报的「很多程序文字没居中、很靠上」):
     * 这一份的控件是**从 GY 往下逐项堆**的,最后一行落在 GY+276 附近 ⇒ 整块高约 300。
     * 原先写死 `GY = 14` ⇒ 内容只占画布上部三成,下面一大条空白 —— 看着就是"全挤在顶上"。
     * 居中之后顶部与底部留白相等(`SH` 是**画布**高,见 `relayout` 开头)。
     * ⚠ 下限保 14:画布特别矮时(横屏分屏)别把内容推出上边缘。
     * Center the whole block vertically (user report, 2026-10-07). The widgets are stacked downward
     * from GY and the last line sits near GY+276, so the block is about 300 tall; with a fixed
     * `GY = 14` it occupied only the top third. Clamp at 14 so a very short canvas cannot push it off. */
    GY = (SH - 300) / 2;
    if (GY < 14) GY = 14;
}

static void draw()
{
    int cx = SW / 2;
    int pad = SW / 16;
    int i;
    int w;

    ui_2d_clear(0xFF101018);

    // ── 标题 + 方向 + 画布尺寸 ──

    // ── Title + orientation + canvas size ──

    ui_2d_text_styled(cx, GY, gLang == 0 ? "UI 接口 / demo_ui.cpp" : "UI interface / demo_ui.cpp", 0xFFE8E8F0, 16,
                   VML_ANCHOR_CENTER, VML_FONT_BOLD);
    if (g_orient == VML_ORIENT_LANDSCAPE) {
        ui_2d_text(cx, GY + 26, gLang == 0 ? "屏幕方向 = 横屏 (LANDSCAPE)" : "Orientation = LANDSCAPE", 0xFF51E86E, 13, VML_ANCHOR_CENTER);
    } else {
        ui_2d_text(cx, GY + 26, gLang == 0 ? "屏幕方向 = 竖屏 (PORTRAIT)" : "Orientation = PORTRAIT", 0xFF51E86E, 13, VML_ANCHOR_CENTER);
    }
    ui_2d_text(cx, GY + 46, gLang == 0 ? "画布按宿主给的尺寸现排" : "Canvas laid out from the host's size", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER);

    // ── 跟随尺寸的方框:旋转后跟着变宽变矮,这就是"不写死坐标"的证明 ──

    // ── A box that follows the size: after rotation it gets wider and shorter, and that is the proof of "no hardcoded coordinates" ──

    ui_2d_rect(pad, GY + 70, SW - pad * 2, 90, 0xFF1A1A24, 1, 0, 10);
    ui_2d_rect(pad + 6, GY + 76, SW - pad * 2 - 12, 30, 0xFF4A90D9, 1, 0, 6);
    ui_2d_text(pad + 16, GY + 84, gLang == 0 ? "rect / round-rect(随屏宽伸缩)" : "rect / round-rect (stretches with the width)", 0xFF101018, 12,
            VML_ANCHOR_LEFT);

    // ── 圆 / 椭圆 / 直线 ──

    // ── Circle / ellipse / line ──

    ui_2d_circle(cx - SW / 6, GY + 140, SW / 12, 0xFFE06C50, 1, 0);
    ui_2d_ellipse(cx + SW / 6, GY + 140, SW / 9, SW / 18, 0xFFD9B44A, 1, 0);
    ui_2d_line(pad, GY + 172, SW - pad, GY + 172, 0xFF50C878, 3);

    // ── 8 格真彩色带 ──

    // ── The 8-cell true-color band ──

    w = (SW - pad * 2) / 8;
    for (i = 0; i < 8; i++) {
        if ((i & 1) != 0) {
            ui_2d_rect(pad + i * w, GY + 186, w - 2, 16, 0xFF4A90D9, 1, 0, 2);
        } else {
            ui_2d_rect(pad + i * w, GY + 186, w - 2, 16, 0xFFE06C50, 1, 0, 2);
        }
    }
    ui_2d_text(cx, GY + 210, gLang == 0 ? "真彩 0xAARRGGBB(不是索引色)" : "True color 0xAARRGGBB (not indexed)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER);

    // ── 事件计数 ──

    // ── Event counters ──

    ui_2d_text(pad, GY + 236, gLang == 0 ? "帧" : "Frames", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT);
    ui_2d_text(pad + 22, GY + 236, numstr(g_frames), 0xFFFFFFFF, 13, VML_ANCHOR_LEFT);
    ui_2d_text(pad, GY + 256, gLang == 0 ? "按键" : "Keys", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT);
    ui_2d_text(pad + 44, GY + 256, numstr(g_keys), 0xFFFFFFFF, 13, VML_ANCHOR_LEFT);
    ui_2d_text(pad, GY + 276, gLang == 0 ? "触摸" : "Touches", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT);
    ui_2d_text(pad + 44, GY + 276, numstr(g_touches), 0xFFFFFFFF, 13, VML_ANCHOR_LEFT);

    // ── 触摸标记:点哪儿就在哪儿留个圈 ──

    // ── Touch marks: wherever you tap, a circle is left there ──

    if (g_tx >= 0) {
        ui_2d_circle(g_tx, g_ty, 18, 0xFFFFE060, 0, 2);
        ui_2d_circle(g_tx, g_ty, 4, 0xFFFFE060, 1, 0);
        ui_2d_text(cx, SH - 44, gLang == 0 ? "触摸坐标已经用上了" : "Touch coordinates are in use", 0xFFFFE060, 12, VML_ANCHOR_CENTER);
    } else {
        ui_2d_text(cx, SH - 44, gLang == 0 ? "点一下屏幕 / 按任意键退出" : "Tap the screen / press any key to exit", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER);
    }

    ui_2d_text(cx, SH - 24, gLang == 0 ? "退出:按任意键或点任意处(或等 N 帧到点)" : "Exit: any key or any tap (or wait out the frame count)",
            0xFF9AA0B0, 12, VML_ANCHOR_CENTER);

    ui_2d_present();
}

int main()
{
    int msg[4];
    int tid;
    int done = 0;
    int t;

    // ── ① 开窗**之前**就问方向 ──

    // ── ① Ask for the orientation **before** opening the window ──

    g_orient = ui_scr_get_orientation();
    gLang = ui_scr_get_language();

    // 开窗:尺寸传 0 ⇒ **交给宿主用真实可用区**。开窗**之前** `ui_scr_get_w/h` 只能估算,

    // Open the window: pass size 0 ⇒ **the host uses the real available area**. **Before** the window opens,

    // 估算与实际总有出入(这就是"内容偏上"的根因);开窗后 `relayout()` 再问一次拿到的是**真实画布**。

    // `ui_scr_get_w/h` can only estimate (that is exactly why the content sat high); after the open, `relayout()` re-queries and gets the **real canvas**.

    // 其余声明照旧:"支持旋转 + 要手柄"(转屏时宿主会把新坐标空间整个给过来)。

    // The rest is unchanged: "supports rotation + wants the gamepad" (on rotation the host hands over the whole new coordinate space).

    ui_win_open_ex("demo_ui.cpp", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NEED_GAMEPAD);
    relayout();
    draw();

    // ── ⑤ 消息循环:定时器驱动重画,**有界退出** ──

    // ── ⑤ Message loop: the timer drives the redraws, **bounded exit** ──

    tid = ui_timer_set(60, 1);
    while (done == 0 && ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 0);
        if (t == VML_MSG_NONE) continue;

        if (t == VML_MSG_TIMER) {
            g_frames = g_frames + 1;
            draw();
            if (g_frames >= MAX_FRAMES) done = 1;      // ← 有界:到点自己停

            // ← bounded: stops by itself when the frame budget runs out

            continue;
        }

        if (t == VML_MSG_KEYDOWN) {
            g_keys = g_keys + 1;
            draw();
            done = 1;                                  // ← 有界:收到键就退

            // ← bounded: exits as soon as a key arrives

            continue;
        }

        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
            g_touches = g_touches + 1;
            g_tx = msg[1];                             // msg[1]=x msg[2]=y

            g_ty = msg[2];
            draw();
            done = 1;                                  // ← 有界:点到就退

            // ← bounded: exits as soon as a tap arrives

            continue;
        }

        if (t == VML_MSG_WINDOWORIENT) {
            g_orient = msg[1];                         // A = 新方向

            // A = the new orientation

            continue;
        }

        if (t == VML_MSG_WINDOWRESIZE) {
            relayout();                                // 换坐标系了 ⇒ 重排 + 重画

            // the coordinate system changed ⇒ re-layout + redraw

            draw();
            continue;
        }

        if (t == VML_MSG_WINDOWCLOSE) break;           // 用户把窗口关了

        // the user closed the window

    }

    ui_timer_kill(tid);
    ui_win_close();

    // 给无头验证留一行确定性判据(图形只能肉眼看,这几个数能自动比)

    // Leave one line of deterministic criteria for headless verification (graphics can only be eyeballed; these numbers can be compared automatically)

    if (g_orient == VML_ORIENT_LANDSCAPE) {
        printf("demo_ui: orient=LANDSCAPE\n");
    } else {
        printf("demo_ui: orient=PORTRAIT\n");
    }
    printf("demo_ui: canvas=%dx%d\n", SW, SH);
    // ⚠ 三次 `numstr` **必须分开 printf**:它返回的是**同一个静态缓冲区**,

    // ⚠ The three `numstr` calls **must be printed with separate printf calls**: it returns **the same static buffer**,

    //   三个一起当实参传进去会全部指向那块内存、打印出同一个值(经典别名坑)。

    //   so passing all three together as arguments makes them all point at that memory and print the same value (the classic aliasing trap).

    printf("demo_ui: frames=%s\n", numstr(g_frames));
    printf("demo_ui: keys=%s\n", numstr(g_keys));
    printf("demo_ui: touches=%s\n", numstr(g_touches));
    printf("demo_ui: done\n");
    return 0;
}