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

lua/ui/demo_ui.lua · 17093 字节 · 下载原文

-- demo_ui.lua —— **第 4 层:最新 UI 接口**(`ui_*`)

-- demo_ui.lua — **Layer 4: the newest UI interface** (`ui_*`)

--

-- 这一层和第 3 层(BGI)正好相反:

-- This layer is the exact opposite of layer 3 (BGI):

--

--   · **没有固定分辨率**。画布多大由宿主给(`ui_scr_get_w()` / `ui_scr_get_h()`),

--   · **There is no fixed resolution.** The host decides how large the canvas is (`ui_scr_get_w()` / `ui_scr_get_h()`),

--     程序按拿到的尺寸现排版 —— 手机上还有手柄区收放、横竖屏切换会让它变。

--     and the program lays itself out from the size it got — on a phone the gamepad area collapsing/expanding and portrait/landscape switching make it change.

--   · **颜色是真彩**(`0xAARRGGBB`),不是 16 个索引色。

--   · **Colors are truecolor** (`0xAARRGGBB`), not 16 indexed colors.

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

--   · **There is a message loop.** Touches, key presses, timers, the window being closed / rotated all come out of `ui_msg_fetch_wait`,

--     程序按返回值分派 —— 这是"能交互的程序"和"画完就死"的分水岭。

--     and the program dispatches on the return value — this is the watershed between "an interactive program" and "draws once and dies".

--

-- ## 这个 demo 演示什么

-- ## What this demo demonstrates

--

--   ① **开窗前先问屏幕方向**(`ui_scr_get_orientation()`)—— 程序据此决定排版。

--   ① **Ask the screen orientation before opening the window** (`ui_scr_get_orientation()`) — the program decides its layout from it.

--      方向是**设备**的属性,别拿 `ui_scr_get_w() > ui_scr_get_h()` 去推(那是可用绘图区,

--      Orientation is a property of the **device**; do not infer it from `ui_scr_get_w() > ui_scr_get_h()` (that is the usable drawing area,

--      手柄收起/展开会变)。

--      which changes when the gamepad is collapsed/expanded).

--   ② 开窗 → 拿画布尺寸 → 按尺寸现排(**没有写死的坐标**)。

--   ② Open the window → get the canvas size → lay out from that size (**no hard-coded coordinates**).

--   ③ 画图元:矩形 / 圆角矩形 / 圆 / 椭圆 / 直线 / 文字,位置全部由 w/h 算出来。

--   ③ Draw primitives: rectangle / rounded rectangle / circle / ellipse / line / text, with every position computed from w/h.

--   ④ `ui_2d_present()` 交帧 —— 它是"这一帧画完了"的唯一信号。

--   ④ `ui_2d_present()` submits the frame — it is the only signal that "this frame is finished".

--   ⑤ **消息循环**:定时器驱动重画、按键 / 触摸有响应、**有界退出**

--   ⑤ **Message loop**: a timer drives redraws, keys / touches respond, and it **exits on a bound**

--      (收够 N 帧 或 按任意键 / 点任意处)。

--      (after collecting enough frames, or on any key / any tap).

--

-- ## ⚠ 本前端的两条限制(决定了几处写法)

-- ## ⚠ Two limits of this frontend (they decide the style in a few places)

--

--   · ~~**`..` 字符串拼接编不过**(链到库里没有的 `lua_concat`)~~ ✅ **已修好(2026-10-04 复验)** ——

--   · ~~**`..` string concatenation does not compile** (it links to `lua_concat`, which the library does not have)~~ ✅ **Fixed (re-verified 2026-10-04)** —

--     `..` 能编过、结果也对(`local s = "AB" .. "CD"` + `print_str(s)` ⇒ `ABCD`);⚠ 要打它得用

--     `..` compiles and gives the right result (`local s = "AB" .. "CD"` + `print_str(s)` => `ABCD`); ⚠ print it with

--     `print_str` —— `print` 拿到非字面量字符串打的不是那个串(实测 ⇒ `46825`)。⇒ 颜色与坐标仍写字面量(写法无害)。

--     `print_str` — `print` given a non-literal string does not print it (measured => `46825`). ⇒ colors and coordinates are still literals (harmless).

--   · 颜色写**负数十进制**(`life.lua` 的规矩,与 `0xAARRGGBB` 同值):

--     `0xFF101018` = `-15724520`。

--   · ~~循环变量**先 `local` 声明**再进循环(v0.96.202 修的就是这条)。~~

--   · ~~The loop variable is **declared with `local`** before entering the loop (this is exactly what v0.96.202 fixed).~~

--     ✅ **已修好(2026-10-04 复验)**:不预先 `local` 也能跑。

--     ✅ **Fixed (re-verified 2026-10-04)**: it runs without a prior `local`.

--   · 好消息:`break` 在本前端**是好的**(实测 `while` 里嵌 `if` 再 `break` 正常退出)。

--   · Good news: `break` **works** on this frontend (measured: `while` containing `if` and then `break` exits normally).

--

-- 跑法:

-- How to run:

--   手机    vml run examples/lua/demo_ui.lua

--   phone   vml run examples/lua/demo_ui.lua

--   桌面    vmlcli Examples/lua/demo_ui.lua --screen 640x480 --frames /tmp/fr

--   desktop vmlcli Examples/lua/demo_ui.lua --screen 640x480 --frames /tmp/fr

--   喂输入  … --input 脚本(一行一条,如 `200 touchdown 300 200` / `150 keydown 13`)

--   feed input  … --input script (one line each, e.g. `200 touchdown 300 200` / `150 keydown 13`)


-- ── 显式引用 UI 库的声明文件 ────────────────────────────────────────────────

-- Explicit reference to the UI library's declaration file.

--

-- `Lib/lua/vml_ui.lua` 由 `GenLib -n -c vmlui -l lua` 从 `Lib/shared/src/vmlui.c` 生成(155 个 `ui_*`)。

-- `Lib/lua/vml_ui.lua` is produced by `GenLib -n -c vmlui -l lua` from `Lib/shared/src/vmlui.c` (155 `ui_*`).

-- **不要手写、也不要手改生成物**(要改就改生成器 `tools/GenLib`)。

-- Never hand-write it and never edit the generated file (change the generator `tools/GenLib` instead).

--

-- ⚠ 这条 `require` 对 Lua 是**文档级**引用:本前端按名字解析库函数,删掉它照样编得过、跑得对。

-- ⚠ For Lua this `require` is a **documentation-level** reference: the front end resolves library

-- functions by name, so removing it still compiles and runs.

-- 实测结论与命令见 `docs/UI库导入约定.md` 的「lua 实测结论」一节。

-- Measured results and the exact commands are in the UI-library import convention doc (Lua section).

require("vml_ui")
local c_bg = -15724520        -- 0xFF101018

local c_panel = -15066588     -- 0xFF1A1A24

local c_title = -1513232      -- 0xFFE8E8F0

local c_ok = -11409298        -- 0xFF51E86E

local c_dim = -6643536        -- 0xFF9AA0B0

local c_blue = -11890471      -- 0xFF4A90D9

local c_orange = -2069424     -- 0xFFE06C50

local c_gold = -2509750       -- 0xFFD9B44A

local c_green = -11483016     -- 0xFF50C878

local c_mark = -8096          -- 0xFFFFE060


-- ── ① 开窗**之前**就问方向:程序据此决定排版 ──

-- ── ① Ask the orientation **before** opening the window: the program decides its layout from it ──

local orient = ui_scr_get_orientation()
local lang = ui_scr_get_language()

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

-- Opening the window: pass size 0 ⇒ **the host uses the real available area**. **Before** the open,

-- 实际总有出入(这就是"内容偏上"的根因)⇒ 所以尺寸改在**开窗之后**问(那时才是真实画布)。

-- `ui_scr_get_w/h` can only estimate and the estimate differs from reality (exactly why the content sat high) ⇒ the size is now queried **after** the open, when it is the real canvas.

ui_win_open("demo_ui (Lua)", 0, 0)
ui_scr_set_keep_on(1)

-- 尺寸:开窗之后再问一次(拿不到就退到竖屏默认值)

-- Size: re-queried after the open (falling back to the portrait defaults if unavailable)

local w = ui_scr_get_w()
local h = ui_scr_get_h()
if w <= 0 then
  w = 360
end
if h <= 0 then
  h = 620
end

-- 内容整块**在画布内垂直居中**(用户 2026-10-07 报的「很多程序文字没居中、很靠上」):

-- 这一份的控件是**从 y=12 往下逐项堆**的:最后一项(8 格真彩色带)落在 y=384..398,下面那行

-- "触摸"文字在 y=396 ⇒ 整块从标题字形顶(≈ 0)到最后约 399 ⇒ 按 **400** 算。

-- 下面每一条 y 都加 `gy`(`h` 是**画布**高)。⚠ 下限保 0:画布特别矮时别把内容推出上边缘。

-- ⚠ 最后两行(`h - 60` / `h - 36`)是**钉在底部的**,不动 —— 与样板 `c/ui/demo_ui.c` 的

--   `SH - 44` / `SH - 24` 同理;触摸标记画在**用户自己点的那个坐标**上,更不能加偏移。

-- Center the whole block vertically (user report, 2026-10-07): the widgets are stacked downward from

-- y=12 and the last item (the 8-cell band) sits at y=384..398 with the "touches" row at y=396, so the

-- block runs from the title's ascender (~0) to ~399 => use **400**. Every y below is offset by `gy`

-- (`h` is the **canvas** height); clamp at 0 so a short canvas cannot push the content off the top.

-- The two footer rows (`h - 60` / `h - 36`) stay pinned to the bottom (same idea as `SH - 44` /

-- `SH - 24` in the `c/ui/demo_ui.c` model), and the touch marker uses the user's own tap coordinates.

local gy = (h - 400) / 2
if gy < 0 then
  gy = 0
end

-- 定时器:每 60ms 一拍,驱动重画(也是"没输入也能自己停"的那个节拍源)

-- Timer: one tick every 60ms, driving redraws (also the beat source that lets it stop on its own with "no input")

local tid = ui_timer_set(60, 1)

local frames = 0
local keys = 0
local touches = 0
local tx = 0 - 1          -- 最近一次触摸/点击的位置(没点过就是 -1)

-- the position of the most recent touch/click (-1 if never tapped)

local ty = 0
local done = 0

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

-- ── ⑤ Message loop: a timer drives redraws, exits on a bound ──

while ui_win_is_closed() == 0 do
  if done ~= 0 then
    break
  end

  local t = ui_msg_fetch_wait(0)      -- 阻塞等一条(有定时器在 ⇒ 一定会返回)

  -- block until one message arrives (a timer is armed ⇒ it is bound to return)


  if t == 9 then
    frames = frames + 1
  end
  if t == 1 then
    keys = keys + 1
    done = 1                    -- ← 有界:收到键就退

    -- ← bounded: exit as soon as a key arrives

  end
  if t == 6 then
    touches = touches + 1
    tx = ui_msg_get_a()             -- A = x

    ty = ui_msg_get_b()             -- B = y

    done = 1                    -- ← 有界:点到就退

    -- ← bounded: exit as soon as there is a tap

  end
  if t == 4 then
    touches = touches + 1
    tx = ui_msg_get_a()
    ty = ui_msg_get_b()
    done = 1
  end
  if t == 10 then
    break                       -- 用户把窗口关了

    -- the user closed the window

  end

  -- ── 画这一帧(每个位置都由 w / h 现算,没有写死的坐标)──

  -- ── Draw this frame (every position is computed from w / h on the spot, no hard-coded coordinates) ──

  local pad = w / 16
  local cx = w / 2

  ui_2d_clear(c_bg)

  if lang == 0 then ui_2d_text(cx, gy + 12, "UI 接口 / demo_ui (Lua)", c_title, 16, VML_ANCHOR_CENTER)
  else ui_2d_text(cx, gy + 12, "UI API / demo_ui (Lua)", c_title, 16, VML_ANCHOR_CENTER) end
  if orient == 1 then
    if lang == 0 then ui_2d_text(cx, gy + 36, "屏幕方向 = 横屏 (LANDSCAPE)", c_ok, 13, VML_ANCHOR_CENTER)
    else ui_2d_text(cx, gy + 36, "Orientation = LANDSCAPE", c_ok, 13, VML_ANCHOR_CENTER) end
  end
  if orient ~= 1 then
    if lang == 0 then ui_2d_text(cx, gy + 36, "屏幕方向 = 竖屏 (PORTRAIT)", c_ok, 13, VML_ANCHOR_CENTER)
    else ui_2d_text(cx, gy + 36, "Orientation = PORTRAIT", c_ok, 13, VML_ANCHOR_CENTER) end
  end
  if lang == 0 then ui_2d_text(cx, gy + 56, "画布按宿主给的尺寸现排", c_dim, 12, VML_ANCHOR_CENTER)
  else ui_2d_text(cx, gy + 56, "Canvas laid out from the host-provided size", c_dim, 12, VML_ANCHOR_CENTER) end

  -- 一个跟随尺寸的方框(转屏后它会跟着变宽变矮 —— 这就是"不写死坐标"的证明)

  -- A box that follows the size (after a rotation it grows wider and shorter — proof of "no hard-coded coordinates")

  ui_2d_rect(pad, gy + 76, w - pad * 2, 84, c_panel, 1, 0, 10)
  ui_2d_rect(pad + 6, gy + 82, w - pad * 2 - 12, 28, c_blue, 1, 0, 6)
  if lang == 0 then ui_2d_text(pad + 16, gy + 88, "rect / 圆角矩形(随屏宽伸缩)", c_bg, 12, VML_ANCHOR_LEFT)
  else ui_2d_text(pad + 16, gy + 88, "rect / rounded rect (stretches with the screen width)", c_bg, 12, VML_ANCHOR_LEFT) end

  -- 圆 / 椭圆 / 直线:三个基本形

  -- Circle / ellipse / line: the three basic shapes

  ui_2d_circle(cx - w / 6, gy + 200, w / 12, c_orange, 1, 0)
  ui_2d_ellipse(cx + w / 6, gy + 200, w / 9, w / 18, c_gold, 1, 0)
  ui_2d_line(pad, gy + 250, w - pad, gy + 250, c_green, 3)

  -- 一条 8 格真彩色带(这些是 RGB,不是调色板索引)

  -- An 8-cell truecolor band (these are RGB, not palette indexes)

  local bw = (w - pad * 2) / 8
  local i = 0
  while i < 8 do
    local c = c_blue
    if i % 2 == 1 then
      c = c_orange
    end
    ui_2d_rect(pad + i * bw, gy + 266, bw - 2, 18, c, 1, 0, 2)
    i = i + 1
  end
  if lang == 0 then ui_2d_text(cx, gy + 296, "真彩 0xAARRGGBB(不是索引色)", c_dim, 12, VML_ANCHOR_CENTER)
  else ui_2d_text(cx, gy + 296, "truecolor 0xAARRGGBB (not a palette index)", c_dim, 12, VML_ANCHOR_CENTER) end

  -- 事件计数:不画数字,画**长度随计数增长的条**

  -- Event counters: instead of drawing numbers, draw **bars whose length grows with the count**

  -- (`..` 用不了 ⇒ 数字没法拼进文本;画成条反而一眼看出"事件到了没有")

  -- (`..` is unusable ⇒ numbers cannot be spliced into text; drawing them as bars makes it obvious at a glance whether events have arrived)

  if lang == 0 then ui_2d_text(pad, gy + 336, "帧", c_dim, 12, VML_ANCHOR_LEFT)
  else ui_2d_text(pad, gy + 336, "frames", c_dim, 12, VML_ANCHOR_LEFT) end
  local bw2 = frames * 6
  if bw2 > w - pad * 2 - 40 then
    bw2 = w - pad * 2 - 40
  end
  ui_2d_rect(pad + 40, gy + 324, bw2, 14, c_blue, 1, 0, 3)

  if lang == 0 then ui_2d_text(pad, gy + 366, "按键", c_dim, 12, VML_ANCHOR_LEFT)
  else ui_2d_text(pad, gy + 366, "keys", c_dim, 12, VML_ANCHOR_LEFT) end
  local bw3 = keys * 30
  if bw3 > w - pad * 2 - 60 then
    bw3 = w - pad * 2 - 60
  end
  ui_2d_rect(pad + 60, gy + 354, bw3, 14, c_orange, 1, 0, 3)

  if lang == 0 then ui_2d_text(pad, gy + 396, "触摸", c_dim, 12, VML_ANCHOR_LEFT)
  else ui_2d_text(pad, gy + 396, "touches", c_dim, 12, VML_ANCHOR_LEFT) end
  local bw4 = touches * 30
  if bw4 > w - pad * 2 - 60 then
    bw4 = w - pad * 2 - 60
  end
  ui_2d_rect(pad + 60, gy + 384, bw4, 14, c_gold, 1, 0, 3)

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

  -- Touch marker: wherever you tap, a circle is left there (proof that the coordinates really work)

  if tx >= 0 then
    ui_2d_circle(tx, ty, 18, c_mark, 0, 2)
    ui_2d_circle(tx, ty, 4, c_mark, 1, 0)
    if lang == 0 then ui_2d_text(cx, h - 60, "触摸坐标已经用上了", c_mark, 12, VML_ANCHOR_CENTER)
    else ui_2d_text(cx, h - 60, "The touch coordinates are in use", c_mark, 12, VML_ANCHOR_CENTER) end
  end
  if tx < 0 then
    if lang == 0 then ui_2d_text(cx, h - 60, "点一下屏幕 / 按任意键退出", c_dim, 12, VML_ANCHOR_CENTER)
    else ui_2d_text(cx, h - 60, "Tap the screen / press any key to exit", c_dim, 12, VML_ANCHOR_CENTER) end
  end

  if lang == 0 then ui_2d_text(cx, h - 36, "退出:按任意键或点任意处(或等 N 帧到点)", c_dim, 12, VML_ANCHOR_CENTER)
  else ui_2d_text(cx, h - 36, "Exit: press any key or tap anywhere (or wait N frames)", c_dim, 12, VML_ANCHOR_CENTER) end

  ui_2d_present()

  if frames >= 30 then
    done = 1                    -- ← 有界:到点自己停

    -- ← bounded: it stops itself once the target is reached

  end
end

-- ── 收尾:定时器要杀,屏幕常亮要还回去 ──

-- ── Wrap-up: the timer must be killed and screen-always-on must be given back ──

ui_timer_kill(tid)
ui_scr_set_keep_on(0)
ui_win_close()

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

-- Leave a few **deterministic** checks for headless verification (the graphical part can only be eyeballed; these numbers compare automatically)

print("demo_ui: orient=", orient)
print("demo_ui: canvas=", w, " x ", h)
print("demo_ui: frames=", frames, " keys=", keys, " touches=", touches)
print("demo_ui: done")