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

kotlin/ui/demo_ui.kt · 18945 字节 · 下载原文

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

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

//

// 与第 3 层(BGI)正好相反:

// Exactly the opposite of layer 3 (BGI):

//

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

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

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

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

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

//   · **There is a message loop**: touch / key / timer / window closed / rotated all arrive as messages.

//

// ## ⚠ 两条写法约束(都是实测出来的)

// ## ⚠ Two style constraints (both measured)

//

// ### ① 消息用**标量**版读,不把数组传给库函数

// ### ① Read messages with the **scalar** version; don't pass arrays to library functions

//

// `catch.kt` 记的是「文件级 `arrayOf` 读回 0」;更普遍的一条是:**数组传给库的 `int*`

// `catch.kt` records "a file-level `arrayOf` reads back 0"; the more general one is: **when an array is passed to a library's `int*`

// 形参时指针落在数据起点前 4 字节**("长度头")—— 实测 `ui_msg_wait(msg,…)` 时库里写

// parameter the pointer lands 4 bytes before the data start** (the "length header") — measured with `ui_msg_wait(msg,…)`: the library writes

// `msg=(类型,A,B,时间戳)`,Kotlin 这边读到 `(A,B,时间戳,旧值)`(整体错位一格)。

// `msg=(type,A,B,timestamp)` and the Kotlin side reads `(A,B,timestamp,old value)` (shifted by one slot overall).

// 所以这一层在 Kotlin 里只用**标量**版:`ui_msg_fetch_wait` / `ui_msg_get_a` / `ui_msg_get_b`

// So on Kotlin this layer only uses the **scalar** versions: `ui_msg_fetch_wait` / `ui_msg_get_a` / `ui_msg_get_b`

// —— `vml_ui.h` 里那组"不碰指针的消息读取(非 C 语言用)"正是为这个准备的。

// —— That set of "pointer-free message readers (for non-C languages)" in `vml_ui.h` exists precisely for this.

// 同理 `ui_2d_polygon(int* pts, …)` 这类收数组的图元在 Kotlin 里不能用,本 demo 不碰。

// Likewise array-taking primitives such as `ui_2d_polygon(int* pts, …)` cannot be used from Kotlin, and this demo does not touch them.

// ✅ 2026-10-04 复验仍成立:`callwithint8(arrayOf(1,11,…,77))` Kotlin 侧得 **-6**、C 侧得 84197531(数组传给库的 `int*` 形参仍然对不上)。

// ✅ Re-verified 2026-10-04, still holds: `callwithint8(arrayOf(1,11,…,77))` gives **-6** on the Kotlin side vs. 84197531 on the C side (arrays passed to a library `int*` parameter are still misaligned).

//

// ### ② 状态放**文件级 `var`**,数组放 `main` 里

// ### ② State goes in **file-level `var`**, arrays go in `main`

//

// 实测(2026-09-24):文件级 `val TOPVAL = 9` / `var topA` 都能正常读写(读回 9),

// Measured (2026-09-24): file-level `val TOPVAL = 9` / `var topA` both read and write correctly (read back 9),

// 所以标量状态放文件级没问题;而 **~~数组必须留在 `main` 内(`catch.kt` 那条)~~** —— 2026-10-04 复验那条**已修**

// so scalar state at file level is fine; ~~**arrays must stay inside `main`** (the `catch.kt` item)~~ -- re-verified 2026-10-04: that one is **fixed**

// (文件级 `arrayOf` 的 `A[0]+A[3]` 得 10;`val TOPVAL = 9` 读回 9、`var topA = 3` 读回 3)。

// (a file-level `arrayOf` gives `A[0]+A[3]` = 10; `val TOPVAL = 9` reads 9, `var topA = 3` reads 3).

// 本 demo 的数组一个也不用 —— 标量状态正好够。

// This demo uses no arrays at all — scalar state is exactly enough.

//

// ## 退出是有界的

// ## Exit is bounded

//

// 主循环数**定时器拍数**,到 60 拍自己停;按任意键 / 点任意处也能提前退。

// The main loop counts **timer ticks** and stops itself at 60; any keypress / any tap also exits early.

//

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

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

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

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


// ── 状态(文件级标量;数字写死,旁边注释给名字 —— 源:Lib/c/vml_ui.h)──

//   MSG_NONE=0 KEYDOWN=1 TIMER=9 TOUCHDOWN=6 MOUSEDOWN=4

//   WINDOWCLOSE=10 WINDOWRESIZE=11 WINDOWORIENT=12

//   ANCHOR_LEFT=0 CENTER=1 FONT_BOLD=1 ORIENT_PORTRAIT=0 LANDSCAPE=1

//   WIN_ROTATABLE=1 WIN_NEED_GAMEPAD=1

// 界面语言(0=中文 1=英文):开局查一次(`ui_scr_get_language` 是 syscall,别每帧调)

// UI language (0=Chinese 1=English): queried once at start (`ui_scr_get_language` is a syscall, not once per frame)

// ── 显式引用:下面用到的 `ui_*` 来自共享库 vmlui.vml ──────────────────

// ── Explicit reference: the `ui_*` used below come from the shared library vmlui.vml ──

// ⚠ `.*` 必须有:`import vmlui`(无 `.*`)会被 `ExtractImports` 的正则**静默忽略**。

// ⚠ The `.*` is required: `import vmlui` (without `.*`) is silently ignored by `ExtractImports`.

// ⚠ 但整行删掉也照样编、照样跑 —— 前端靠 `ui_` 前缀**自动**链 `vmlui.vml`。

// ⚠ But deleting the whole line still compiles and runs — the frontend auto-links `vmlui.vml` from the `ui_` prefix.

// ⇒ 所以这条是**文档级**的显式声明,不是强制开关(判据见 `FRONTEND_DEFECTS.md` 的 Kotlin 一节)。

// ⇒ So this is a **documentation-level** explicit reference, not a hard switch (see the Kotlin section of `FRONTEND_DEFECTS.md`).

// ✅ 2026-10-04 复验仍成立:三种写法(`import vmlui` / `import vmlui.*` / 整行删掉)产出的指令数完全相同(59377 条)。

// ✅ Re-verified 2026-10-04, still holds: all three forms (`import vmlui` / `import vmlui.*` / line deleted) produce the identical instruction count (59377).

#include "../../../Lib/kotlin/vml_ui.kt"
import vmlui.*

var LANG = 0
var SW = 0
var SH = 0
var GY = 14
var gFrames = 0
var gKeys = 0
var gTouches = 0
var gOrient = 0
var gTx = -1
var gTy = -1

// 颜色写十六进制 `0xAARRGGBB`。

// Colors are written as hex `0xAARRGGBB`.

// 历史:v0.96.592 及之前本前端**不认 `0x` 字面量**(词法器把 `0xFF101820` 切成 `0` + 标识符)。

// History: up to v0.96.592 this frontend did **not** accept `0x` literals (the lexer split `0xFF101820` into `0` plus an identifier).

// 后果分两种:`val c = 0xFF` 报「未声明的变量」;**实参位置**被静默吃掉 ⇒ 颜色编成 0、不报错。

// Two outcomes: `val c = 0xFF` raised "undeclared variable"; in **argument position** it was swallowed silently ⇒ the color compiled to 0 with no error.

// 当时只能用负十进制绕过去。现已修(词法器认 `0x`/`0b`,解析器按 **32 位字**收窄)。

// Negative decimals were the workaround. Fixed now (the lexer accepts `0x`/`0b` and the parser narrows to a **32-bit word**).

// ✅ 2026-10-04 复验仍成立:`0xFF` 得 255、`0xFF101820` 得 -15722464、`0b1011` 得 11,实参位置也不再被吃掉。

// ✅ Re-verified 2026-10-04, still holds: `0xFF` gives 255, `0xFF101820` gives -15722464, `0b1011` gives 11, and argument positions are no longer swallowed.

// 下面那张是当年的对照表,留作记录。

// The table below is the old cross-reference, kept for the record.

//   BG     0xFF101018 = -15724520      PANEL  0xFF1A1A24 = -15066588

//   BLUE   0xFF4A90D9 = -11890471      ORANGE 0xFFE06C50 =  -2069424

//   YELLOW 0xFFD9B44A =  -2509750      GREEN  0xFF50C878 = -11483016

//   TITLE  0xFFE8E8F0 =  -1513232      ACCENT 0xFF51E86E = -11409298

//   MUTED  0xFF9AA0B0 =  -6643536      GOLD   0xFFFFE060 =    -8096


fun 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+278 ⇒ 整块高约 300

    // (与样板 `c/ui/demo_ui.c` 取**同一个数**,两处布局不要各算各的)。

    // ⚠ 按键 / 触摸的标记圆画在 GY+306,但它们**只在有输入时才画**(`if (gKeys > 0)`)——

    //    无输入时根本不在画面上,**不是稳态画面的一部分 ⇒ 不计入总高**(算进去会把整块顶高 30px)。

    // 原先写死 `GY = 14` ⇒ 内容只占画布上部三成。SH 是**画布**高(见上面两行)。

    // ⚠ 下限保 14:画布特别矮时(横屏分屏)别把内容推出上边缘。

    // Center the whole block vertically (user report, 2026-10-07). The widgets are stacked

    // downward from GY and the last text row sits at GY+278 => the block is about 300 tall, the

    // same number the reference sample `c/ui/demo_ui.c` uses. The key / touch marker circles at

    // GY+306 are drawn **only when there is input**, so they are not part of the steady-state

    // block and are excluded. Clamp at 14 so a short canvas cannot push it off the top.

    GY = (SH - 300) / 2
    if (GY < 14) { GY = 14 }
}

fun draw() {
    var cx = SW / 2
    var pad = SW / 16
    var i = 0
    var w = 0

    ui_2d_clear(0xFF101018)

    if (LANG == 0) { ui_2d_text_styled(cx, GY, "UI 接口 / demo_ui.kt", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER, VML_FONT_BOLD) } else { ui_2d_text_styled(cx, GY, "UI interface / demo_ui.kt", 0xFFE8E8F0, 16, VML_ANCHOR_CENTER, VML_FONT_BOLD) }
    if (gOrient == 1) {
        if (LANG == 0) { ui_2d_text(cx, GY + 26, "屏幕方向 = 横屏 (LANDSCAPE)", 0xFF51E86E, 13, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, GY + 26, "Orientation = LANDSCAPE", 0xFF51E86E, 13, VML_ANCHOR_CENTER) }
    } else {
        if (LANG == 0) { ui_2d_text(cx, GY + 26, "屏幕方向 = 竖屏 (PORTRAIT)", 0xFF51E86E, 13, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, GY + 26, "Orientation = PORTRAIT", 0xFF51E86E, 13, VML_ANCHOR_CENTER) }
    }
    if (LANG == 0) { ui_2d_text(cx, GY + 46, "画布按宿主给的尺寸现排(旋转后跟着变)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, GY + 46, "Canvas laid out from the host size (follows rotation)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) }

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

    // A size-following box: on rotation it follows and gets wider and shorter (this 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)
    if (LANG == 0) { ui_2d_text(pad + 16, GY + 84, "rect / round-rect(随屏宽伸缩)", 0xFF101018, 12, VML_ANCHOR_LEFT) } else { ui_2d_text(pad + 16, GY + 84, "rect / round-rect (follows width)", 0xFF101018, 12, VML_ANCHOR_LEFT) }

    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 格真彩色带

    // 8-cell true-color bar

    i = 0
    while (i < 8) {
        w = (SW - pad * 2) / 8
        if (i % 2 == 0) {
            ui_2d_rect(pad + i * w, GY + 186, w - 2, 16, 0xFFE06C50, 1, 0, 2)
        } else {
            ui_2d_rect(pad + i * w, GY + 186, w - 2, 16, 0xFF4A90D9, 1, 0, 2)
        }
        i = i + 1
    }
    if (LANG == 0) { ui_2d_text(cx, GY + 210, "真彩 0xAARRGGBB(不是索引色)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, GY + 210, "true color 0xAARRGGBB (not indexed)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) }

    // ⚠ 屏上不写数字:这里用**条形长度**表达进度,具体数字走 stdout。

    // ⚠ No numbers on screen: progress is expressed here as a **bar length**, and the actual numbers go to stdout.

    //   ~~Kotlin 侧的 `Int` → `String` 要靠 `+` 拼接,而那个是坏的~~ —— 2026-10-04 复验**已修**

    //   ~~on the Kotlin side `Int` → `String` needs `+` concatenation, and that is broken~~ -- **fixed**, re-verified 2026-10-04

    //   (`println("n=" + n)` 得 `n=7`)。本文件保持原样(能跑就别动;新写的代码不必再绕)。

    //   (`println("n=" + n)` gives `n=7`). This file keeps its original form (leave working code alone; new code need not work around it).

    if (LANG == 0) { ui_2d_text(pad, GY + 236, "已跑帧数(条形)", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT) } else { ui_2d_text(pad, GY + 236, "frames drawn (as a bar)", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT) }
    ui_2d_rect(pad, GY + 254, SW - pad * 2, 14, 0xFF1A1A24, 1, 0, 4)
    ui_2d_rect(pad, GY + 254, (SW - pad * 2) * gFrames / 60, 14, 0xFF50C878, 1, 0, 4)

    if (LANG == 0) { ui_2d_text(pad, GY + 278, "按键 / 触摸来了就画一个标记", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT) } else { ui_2d_text(pad, GY + 278, "a marker is drawn per key / touch", 0xFF9AA0B0, 12, VML_ANCHOR_LEFT) }
    if (gKeys > 0) { ui_2d_circle(pad + 20, GY + 306, 14, 0xFFD9B44A, 1, 0) }
    if (gTouches > 0) { ui_2d_circle(pad + 60, GY + 306, 14, 0xFF51E86E, 1, 0) }

    // 触摸标记:点哪儿就在哪儿留个圈(证明坐标真能用)

    // Touch marker: a circle is left wherever you tap (proof the coordinates really work)

    if (gTx >= 0) {
        ui_2d_circle(gTx, gTy, 18, 0xFFD9B44A, 0, 2)
        ui_2d_circle(gTx, gTy, 4, 0xFFD9B44A, 1, 0)
        if (LANG == 0) { ui_2d_text(cx, SH - 44, "触摸坐标已经用上了", 0xFFD9B44A, 12, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, SH - 44, "touch coordinates in use", 0xFFD9B44A, 12, VML_ANCHOR_CENTER) }
    } else {
        if (LANG == 0) { ui_2d_text(cx, SH - 44, "点一下屏幕 / 按任意键退出", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, SH - 44, "Tap anywhere / any key to exit", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) }
    }

    if (LANG == 0) { ui_2d_text(cx, SH - 24, "退出:按任意键或点任意处(或等 N 帧到点)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) } else { ui_2d_text(cx, SH - 24, "Exit: any key or any tap (or wait for N frames)", 0xFF9AA0B0, 12, VML_ANCHOR_CENTER) }

    ui_2d_present()
}

fun main() {
    var done = 0
    var t = 0
    var tid = 0
    var w = 0
    var h = 0

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

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

    gOrient = ui_scr_get_orientation()
    LANG = 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 open,

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

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

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

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

    // ⚠ 下面这两句是**开窗前的估算值**,只用于兜底默认尺寸;开窗后 `relayout()` 会重新问一次。

    // ⚠ The two reads below are the **pre-open estimate**, kept only for the fallback default; `relayout()` re-queries after the open.

    w = ui_scr_get_w()
    h = ui_scr_get_h()
    if (w <= 0) { w = 360 }
    if (h <= 0) { h = 620 }
    ui_win_open_ex("demo_ui.kt", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NEED_GAMEPAD)
    relayout()
    draw()

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

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

    tid = ui_timer_set(60, 1)
    while (done == 0 && ui_win_is_closed() == 0) {
        t = ui_msg_fetch_wait(200)
        if (t == 0) { continue }                    // MSG_NONE


        if (t == 9) {                               // MSG_TIMER

            gFrames = gFrames + 1
            draw()
            if (gFrames >= 60) { done = 1 }          // 有界:到点自己停

            // bounded: stops by itself when the time is up

            continue
        }

        if (t == 1) {                               // MSG_KEYDOWN

            gKeys = gKeys + 1
            draw()
            done = 1                                // 有界:收到键就退

            // bounded: exits as soon as a key arrives

            continue
        }

        if (t == 6 || t == 4) {                     // MSG_TOUCHDOWN / MSG_MOUSEDOWN

            gTouches = gTouches + 1
            gTx = ui_msg_get_a()                        // A=x B=y

            gTy = ui_msg_get_b()
            draw()
            done = 1                                // 有界:点到就退

            // bounded: exits as soon as a tap arrives

            continue
        }

        if (t == 12) {                              // MSG_WINDOWORIENT

            gOrient = ui_msg_get_a()                    // A = 新方向

            // A = the new orientation

            continue
        }

        if (t == 11) {                              // MSG_WINDOWRESIZE

            relayout()                              // 换坐标系了 ⇒ 重排 + 重画

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

            draw()
            continue
        }

        if (t == 10) { break }                      // MSG_WINDOWCLOSE

    }

    ui_timer_kill(tid)
    ui_win_close()

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

    // Deterministic checkpoints for headless verification (graphics can only be eyeballed, but these numbers can be compared automatically)

    if (gOrient == 1) {
        println("demo_ui: orient=LANDSCAPE")
    } else {
        println("demo_ui: orient=PORTRAIT")
    }
    print("demo_ui: canvas="); print(SW); print("x"); println(SH)
    print("demo_ui: frames="); println(gFrames)
    print("demo_ui: keys=");   println(gKeys)
    print("demo_ui: touches="); println(gTouches)
    println("demo_ui: done")
}