csharp/ui/demo_ui.cs · 15560 字节 · 下载原文
// demo_ui.cs —— **第 4 层:最新 UI 接口**(`ui_*` / `Lib/c/vml_ui.h`)
// demo_ui.cs — **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.
//
// ## ⚠ 消息用**标量**版读,不把数组传给库函数
// ## ⚠ Read messages with the **scalar** version; don't pass arrays to library functions
//
// C# 的 `int[]` 传给库里 `int*` 形参时**指针落在数据起点前 4 字节**("长度头")上。
// When a C# `int[]` is passed to a library `int*` parameter the **pointer lands 4 bytes before the data start** (the "length header").
// 实测(2026-09-24):`callwithint8({1,11,22,…})` —— C 侧返回 `84197531`、
// Measured (2026-09-24): `callwithint8({1,11,22,…})` — the C side returns `84197531`,
// **C# 侧返回 -6**(宿主"没有这个调用号" ⇒ 它读到的调用号不是 1)。同理 `ui_msg_wait(msg,…)`
// **the C# side returns -6** (the host says "no such call number" ⇒ the call number it read wasn't 1). Likewise the
// 里宿主写的 `msg=(类型,A,B,时间戳)` 会被错位读到。
// `msg=(type,A,B,timestamp)` the host writes in `ui_msg_wait(msg,…)` is read back shifted.
//
// 所以这一层在 C# 里只用**标量**版:`ui_msg_fetch_wait` / `ui_msg_get_a` / `ui_msg_get_b`
// So on C# 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, …)` 这类收数组的图元在 C# 里不能用,本 demo 不碰。
// Likewise array-taking primitives such as `ui_2d_polygon(int* pts, …)` cannot be used from C#, and this demo does not touch them.
//
// ## 退出是有界的
// ## 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/csharp/demo_ui.cs`;
// How to run: on the phone `vml run examples/csharp/demo_ui.cs`;
// 桌面 `vmlcli Examples/csharp/demo_ui.cs --frames /tmp/fr`
// on the desktop `vmlcli Examples/csharp/demo_ui.cs --frames /tmp/fr`
// 显式引用 UI 库:`ui_*` 的声明在 `Lib/csharp/VmlUi.cs`(tools/GenLib 生成,别手改)。
// Explicit reference to the UI library: `ui_*` is declared in `Lib/csharp/VmlUi.cs` (generated by tools/GenLib; don't hand-edit).
using Vml.Ui;
class DemoUi
{
// ── 消息类型 / 锚点 / 方向 / 字体样式(源:Lib/c/vml_ui.h)──
// ── Message types / anchors / orientation / font styles (source: Lib/c/vml_ui.h) ──
const int MSG_NONE = 0;
const int MSG_KEYDOWN = 1;
const int MSG_TOUCHDOWN = 6;
const int MSG_MOUSEDOWN = 4;
const int MSG_TIMER = 9;
const int MSG_WINDOWCLOSE = 10;
const int MSG_WINDOWRESIZE = 11;
const int MSG_WINDOWORIENT = 12;
const int ANCHOR_LEFT = 0;
const int ANCHOR_CENTER = 1;
const int FONT_BOLD = 1;
const int ORIENT_LANDSCAPE = 1;
const int WIN_ROTATABLE = 1;
const int WIN_NEED_GAMEPAD = 1;
const int MAX_FRAMES = 60; // 到点自己停:60 拍 × 60ms ≈ 3.6 秒
// stops by itself when the time is up: 60 ticks × 60ms ≈ 3.6 seconds
// ── 颜色(`0xAARRGGBB` 的十进制负数形式,与仓库里其它语言例程一致)──
// ── Colors (the decimal-negative form of `0xAARRGGBB`, consistent with the other language examples in the repo) ──
const int BG = -15724520; // 0xFF101018
const int PANEL = -15066588; // 0xFF1A1A24
const int BLUE = -11890471; // 0xFF4A90D9
const int ORANGE = -2069424; // 0xFFE06C50
const int YELLOW = -2509750; // 0xFFD9B44A
const int GREEN = -11483016; // 0xFF50C878
const int TITLE = -1513232; // 0xFFE8E8F0
const int ACCENT = -11409298; // 0xFF51E86E
const int MUTED = -6643536; // 0xFF9AA0B0
// ── 状态 ──
// ── State ──
static int sw;
static int sh;
static int gy;
static int frames;
static int keys;
static int touches;
static int orient;
static int tx = -1;
static int ty = -1;
static int tid;
static int lang; // 界面语言:0=中文 1=英文(开局问一次宿主,见 Main)
// UI language: 0=Chinese 1=English (asked of the host once at the start, see Main)
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+278 ⇒ 整块高约 300
// (与样板 `c/ui/demo_ui.c` 取**同一个数**,两处布局不要各算各的)。
// ⚠ 按键 / 触摸的标记圆画在 gy+306,但它们**只在有输入时才画**(`if (keys > 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;
}
static void draw()
{
int cx = sw / 2;
int pad = sw / 16;
int i = 0;
int w;
ui_2d_clear(BG);
ui_2d_text_styled(cx, gy, lang == 0 ? "UI 接口 / demo_ui.cs" : "UI interface / demo_ui.cs", TITLE, 16, ANCHOR_CENTER, FONT_BOLD);
if (orient == ORIENT_LANDSCAPE)
{
ui_2d_text(cx, gy + 26, lang == 0 ? "屏幕方向 = 横屏 (LANDSCAPE)" : "Orientation = LANDSCAPE", ACCENT, 13, ANCHOR_CENTER);
}
else
{
ui_2d_text(cx, gy + 26, lang == 0 ? "屏幕方向 = 竖屏 (PORTRAIT)" : "Orientation = PORTRAIT", ACCENT, 13, ANCHOR_CENTER);
}
ui_2d_text(cx, gy + 46, lang == 0 ? "画布按宿主给的尺寸现排(旋转后跟着变)" : "Canvas laid out from the host size (follows rotation)", MUTED, 12, ANCHOR_CENTER);
// 跟随尺寸的方框:旋转后跟着变宽变矮
// A size-following box: on rotation it follows and gets wider and shorter
ui_2d_rect(pad, gy + 70, sw - pad * 2, 90, PANEL, 1, 0, 10);
ui_2d_rect(pad + 6, gy + 76, sw - pad * 2 - 12, 30, BLUE, 1, 0, 6);
ui_2d_text(pad + 16, gy + 84, lang == 0 ? "rect / round-rect(随屏宽伸缩)" : "rect / round-rect (stretches with the width)", BG, 12, ANCHOR_LEFT);
ui_2d_circle(cx - sw / 6, gy + 140, sw / 12, ORANGE, 1, 0);
ui_2d_ellipse(cx + sw / 6, gy + 140, sw / 9, sw / 18, YELLOW, 1, 0);
ui_2d_line(pad, gy + 172, sw - pad, gy + 172, GREEN, 3);
// 8 格真彩色带
// 8-cell true-color bar
while (i < 8)
{
w = (sw - pad * 2) / 8;
if (i % 2 == 0) ui_2d_rect(pad + i * w, gy + 186, w - 2, 16, ORANGE, 1, 0, 2);
else ui_2d_rect(pad + i * w, gy + 186, w - 2, 16, BLUE, 1, 0, 2);
i = i + 1;
}
ui_2d_text(cx, gy + 210, lang == 0 ? "真彩 0xAARRGGBB(不是索引色)" : "True color 0xAARRGGBB (not indexed)", MUTED, 12, ANCHOR_CENTER);
// 进度用**条形长度**表达(C# 前端没有 int→string,见 demo_std.cs);
// Progress is expressed as a **bar length** (the C# frontend has no int→string, see demo_std.cs);
// 具体数字走 stdout —— 那边 Console 打印是好的。
// the actual numbers go to stdout — printing there with Console works.
ui_2d_text(pad, gy + 236, lang == 0 ? "已跑帧数(条形)" : "Frames run (bar)", MUTED, 12, ANCHOR_LEFT);
ui_2d_rect(pad, gy + 254, sw - pad * 2, 14, PANEL, 1, 0, 4);
ui_2d_rect(pad, gy + 254, (sw - pad * 2) * frames / MAX_FRAMES, 14, BLUE, 1, 0, 4);
ui_2d_text(pad, gy + 278, lang == 0 ? "按键 / 触摸来了就画一个标记" : "A marker is drawn on key / touch", MUTED, 12, ANCHOR_LEFT);
if (keys > 0) ui_2d_circle(pad + 20, gy + 306, 14, YELLOW, 1, 0);
if (touches > 0) ui_2d_circle(pad + 60, gy + 306, 14, ACCENT, 1, 0);
// 触摸标记:点哪儿就在哪儿留个圈(证明坐标真能用)
// Touch marker: a circle is left wherever you tap (proof the coordinates really work)
if (tx >= 0)
{
ui_2d_circle(tx, ty, 18, YELLOW, 0, 2);
ui_2d_circle(tx, ty, 4, YELLOW, 1, 0);
ui_2d_text(cx, sh - 44, lang == 0 ? "触摸坐标已经用上了" : "Touch coordinates are in use", YELLOW, 12, ANCHOR_CENTER);
}
else
{
ui_2d_text(cx, sh - 44, lang == 0 ? "点一下屏幕 / 按任意键退出" : "Tap the screen / press any key to exit", MUTED, 12, ANCHOR_CENTER);
}
ui_2d_text(cx, sh - 24, lang == 0 ? "退出:按任意键或点任意处(或等 N 帧到点)" : "Exit: any key or any tap (or wait out the frame count)", MUTED, 12, ANCHOR_CENTER);
ui_2d_present();
}
static void Main()
{
int done = 0;
int t;
int w;
int h;
// 界面语言:开局查一次(`ui_scr_get_language` 是 syscall)—— 文案按它取
// UI language: queried once at startup (`ui_scr_get_language` is a syscall) — the strings are picked by it
lang = ui_scr_get_language();
// ── ① 开窗**之前**就问屏幕方向 ──
// ── ① Ask the screen orientation **before** opening the window ──
orient = ui_scr_get_orientation();
// 开窗:尺寸传 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.cs", 0, 0, WIN_ROTATABLE, 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 == MSG_NONE) continue;
if (t == MSG_TIMER)
{
frames = frames + 1;
draw();
if (frames >= MAX_FRAMES) done = 1; // 有界:到点自己停
// bounded: stops by itself when the time is up
continue;
}
if (t == MSG_KEYDOWN)
{
keys = keys + 1;
draw();
done = 1; // 有界:收到键就退
// bounded: exits as soon as a key arrives
continue;
}
if (t == MSG_TOUCHDOWN || t == MSG_MOUSEDOWN)
{
touches = touches + 1;
tx = ui_msg_get_a(); // A=x B=y
ty = ui_msg_get_b();
draw();
done = 1; // 有界:点到就退
// bounded: exits as soon as a tap arrives
continue;
}
if (t == MSG_WINDOWORIENT)
{
orient = ui_msg_get_a(); // A = 新方向
// A = the new orientation
continue;
}
if (t == MSG_WINDOWRESIZE)
{
relayout(); // 换坐标系了 ⇒ 重排 + 重画
// the coordinate space changed ⇒ re-layout + redraw
draw();
continue;
}
if (t == MSG_WINDOWCLOSE) break;
}
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 (orient == ORIENT_LANDSCAPE) println_str("demo_ui: orient=LANDSCAPE");
else println_str("demo_ui: orient=PORTRAIT");
print_str("demo_ui: canvas="); print_int(sw); print_str("x"); println_int(sh);
print_str("demo_ui: frames="); println_int(frames);
print_str("demo_ui: keys="); println_int(keys);
print_str("demo_ui: touches="); println_int(touches);
println_str("demo_ui: done");
}
}