# 多点触控与设备

多指查询、按住判断、方向锁、全屏。

> 完整清单见「[UI 开发](help:vml/ui)」。下面每个接口都带一句用法和一个例子；
> 签名取自 `Lib/c/vml_ui.h`（权威来源）。

### `ui_audio_is_playing(void)`
后台 BGM 还在放吗 → 1/0（没放过、已放完、被停掉都是 0）。
典型用法：等一首放完再进下一段。
```c
ui_audio_play("bgm.mp3", 0);
while (ui_audio_is_playing()) { ui_msg_wait(msg, 200); }
```
### `ui_scr_set_immersive(int on)`
隐藏 / 恢复状态栏与导航栏（全屏游戏）。
```c
ui_scr_set_immersive(1);   /* 进游戏：全屏 */
ui_scr_set_immersive(0);   /* 退出前恢复 */
```
### `ui_key_is_down(int key)`
某个键**此刻**按住没有（“持续按住左移”不必自己维护状态表）。
⚠ 它**治不了丢 KeyUp** —— 手指划出按键范围、系统吃掉 CANCEL 都会让 KeyUp 永远不来，
而查询会一直报“按着”。**连发逻辑仍然要自带刹车**（按了没动两次就停 / 总拍数上限）。
```c
if (ui_key_is_down(VML_KEY_LEFT)) { x = x - 2; }
```
### `ui_scr_lock_orientation(int mode)`
锁定屏幕方向：`VML_LOCK_PORTRAIT`(0) / `VML_LOCK_LANDSCAPE`(1) / `VML_LOCK_AUTO`(2)。
⚠ 与 `ui_win_open_ex` 的 `rotatable` 是**两件事**：那个管“转屏时窗口跟不跟”，这个管“**系统让不让转**”。
```c
ui_scr_lock_orientation(VML_LOCK_LANDSCAPE);   /* 运行中锁成横屏 */
```
### `ui_touch_read(int slot, int* out)`
查第 slot 根手指（0..9）：写进 `out[0]=x out[1]=y out[2]=按下`，返回 1 有效（槽位越界 0）。
**双人同屏 / 虚拟摇杆 / 双指缩放 / 按和弦**靠它。⚠ 这是**轮询**，要自己每帧查。
```c
int t[3];
if (ui_touch_read(0, t) && t[2]) { /* 第 0 根手指按在 (t[0],t[1]) */ }
```
⚠⚠ **触摸的「事件消息」只有槽位 0 会投** —— `ui_msg_wait` 收到的触摸消息永远只描述第 0 根手指。
所以**凡是多指的程序，按下/抬起的判定必须走轮询**（挂个 16~33ms 的定时器，
自己用上一拍的按下状态做边沿检测）。只靠事件消息的话，程序在单指下一切正常、
**一旦同时按第二根手指，第二根就完全看不见** —— 这也正是"按住琴键的另一根手指
点不动按钮"那类怪现象的来源。现成写法见 `examples/c/app/piano.c`。
### `ui_touch_is_down(void)`
上一次 `ui_touch_fetch` 缓存里的“按下”（1/0）。
```c
if (ui_touch_is_down()) { /* … */ }
```
### `ui_touch_fetch(int slot)`
拿不到指针的语言用这个：查一次并缓存，下面三个读缓存（与 `ui_msg_fetch_wait` + `ui_msg_get_a/b` 同一套分工）。
```c
ui_touch_fetch(0);
if (ui_touch_is_down()) { x = ui_touch_get_x(); y = ui_touch_get_y(); }
```
### `ui_touch_get_x(void)`
上一次 `ui_touch_fetch` 缓存里的 x。
```c
int x = ui_touch_get_x();
```
### `ui_touch_get_y(void)`
上一次 `ui_touch_fetch` 缓存里的 y。
```c
int y = ui_touch_get_y();
```

## 传感器（歪手机控制）

### `ui_sensor_read(int kind, int* out)`
读**最新**读数，写进 `out[0..2]`。返回 **1 = 有效**、**0 = 这台设备没有该传感器**。

`kind` 三选一 —— **做倾斜控制用第一个**：

| kind | 是什么 | 单位 |
|---|---|---|
| `VML_SENS_ACCEL`(0) | **加速度**（含重力） | 毫克（`1000` = 1g） |
| `VML_SENS_GYRO`(1) | 角速度（转得多快） | 千分之一度/秒 |
| `VML_SENS_ROTATION`(2) | 融合姿态（俯仰/翻滚/方位） | 千分之一度 |

```c
int a[3];
if (ui_sensor_read(VML_SENS_ACCEL, a)) {
    /* 平放时 a ≈ (0, 0, 1000)；往右歪 30° 时 a[0] ≈ 500 */
    move_x = 0 - a[0] / 40;     /* ⚠ 取负：球往低处滚 */
}
```

⚠⚠ **为什么倾斜用加速度计而不是陀螺仪**：陀螺仪给的是**角速度**，要"当前角度"得自己
积分，而**积分会漂**（几十秒就偏出可用范围）。加速度计静止时读到的就是**重力方向**，
也就是倾斜本身，**不漂**。

⚠⚠ **两个轴都要取负号**。加速度计读的是"**哪条轴朝上**"—— 把右边抬起来 `x` 会**变大**，
而东西该往**低**的那边滑。写反的症状是"歪这边、东西往那边跑"，
看起来完全像传感器装反了，其实就错在这一行。
（这条**已在真机上确认**：右边抬高、球往左滚。）

⚠ **读不到 ≠ 读到 0**：返回 0 才是"没有这个传感器"。混在一起程序会以为"手机放平了"。

### `ui_sensor_is_available(int kind)`
这台设备有没有该传感器 → 1/0。**开窗之前就能问**。

### `ui_sensor_set_rate(int kind, int ms)`
采样间隔（毫秒，0 = 平台默认）。传感器一开就在耗电，间隔越大越省。

### `ui_sensor_calibrate(int kind)`
把**当前姿态**当成零点（"校准水平"）。玩家躺在沙发上玩时"水平"是错的，
退出重进也还是错的 ⇒ 给一个"点一下校准"的入口。

### `ui_sensor_fetch(int kind)` / `ui_sensor_get_x/y/z(void)`
拿不到指针的语言用这一套（与 `ui_touch_fetch` 同构）：
先 `ui_sensor_fetch(0)` 查一次并缓存，再读 `ui_sensor_get_x()` 等。
```c
ok = ui_sensor_fetch(0);
if (ok) { x = ui_sensor_get_x(); y = ui_sensor_get_y(); z = ui_sensor_get_z(); }
```

现成例子：`examples/c/app/tilt.c`（水平仪 + 小球，三个传感器的读数都画在屏幕上）。

## 电量与省电

玩到一半没电是手机游戏最常见的"事故"，而程序自己就能防 —— 电量低了少画点东西、
关掉音效、提醒玩家存档。**不需要任何权限。**

### `ui_battery_read(int* out)`
读电量与充电状态：`out[0]` = 电量 0–100、`out[1]` = 充电中(1)/没充电(0)。
返回 **1 = 有效**、**0 = 这台设备没有电池**（桌面、一直插电的机器）。

```c
int b[2];
if (ui_battery_read(b) && b[0] < 15 && b[1] == 0) {
    low_power_mode();      /* 没在充电、又是低电量 ⇒ 该省了 */
}
```

⚠ **「没有电池」与「电量 0%」是两回事**：返回 0 才是前者。拿 100 或 0 冒充，
程序会以为"电很足"或"电耗尽了"，而真相是"这个问题在这里没有意义"。

⚠ **电量变得很慢**（几分钟才动一格）⇒ 在"每拍跑一次"的逻辑里顺手看一眼就够，
**不必每帧查**。

### `ui_power_is_saving(void)`
系统的**省电模式**开着吗 → 1/0。

⚠ 与"电量低"**不是一回事**：那是**用户明确要求省电**（也可能在电量还有 40% 时手动开的）。
程序据此降帧是照用户的意思办，而不是你在猜。两者**互相独立** —— 满电也可能省电模式开着。

### `ui_battery_fetch(void)` / `ui_battery_get_level(void)` / `ui_battery_is_charging(void)`
拿不到指针的语言用这一套（与 `ui_touch_fetch` 同构）：
先 `ui_battery_fetch()` 查一次并缓存，再读 `ui_battery_get_level()` 等。

现成例子：`examples/c/app/tilt.c` 底部就画着电量那一行。

## 剪贴板与分享

手机游戏的标配：**分享成绩**、**存取档码**（把整局状态编成一小段文本，
玩家自己存到备忘录里，换设备接着玩）。**都不需要权限。**

```c
char code[64];
ui_clipboard_set("SAVE-1a2b3c");                 /* 把存档码交给玩家 */
if (ui_clipboard_get(code, 64) > 0) { /* 读回来接着玩 */ }
ui_share_text("我在拧螺丝里拧了 8 圈！", "分享战绩");
```

### `ui_clipboard_set(char* text)` / `ui_clipboard_get(char* buf, int cap)`
写 / 读系统剪贴板。`set` 返回 1=已交给系统、0=这一端没有剪贴板；
`get` 返回**写入的 UTF-8 字节数**、**-1=剪贴板是空的**。

⚠ **返回值 1 只表示"已经交给系统"**，不表示"玩家已经存上了" ——
写剪贴板在平台上是异步的，宿主不等结果。
⚠ 缓冲区不够时 `get` 返回的是**实际写入的字节数**（不是原文长度）——
**拿它和 `strlen` 比一比就知道有没有被截断**。
⚠ 传空串给 `set` **不会清空剪贴板**（返回 0）—— 那是"没算出来"，
不是你想要的"清空"。

### `ui_share_text(char* text, char* title)`
弹系统分享面板（标题传空串 = 不带）。1=已弹出、0=这一端没有分享面板。
⚠ **不阻塞** —— 只负责把面板弹出来，程序继续跑。

### `ui_url_open(char* url)`
用系统浏览器打开链接。**只认 `http://` 和 `https://`**，别的 scheme 一律返回 0
（放行任意 scheme 等于让程序借系统去打开任意东西）。

⚠ 它会**离开这个应用** —— 调之前先存档。但**别指望宿主替你暂停**：
那是策略，不是机制。

⚠ **返回 0 是"这一端没有这个能力"，不是失败** —— 桌面就没有分享面板。
游戏该照常继续（与"设备没有陀螺仪"同一条处置）。

## 错误码（上一次调用为什么失败）

相机 / 相册、文件选择器、定位这几条**失败时一律返回 0** —— 而 0 分不清三件事：
**用户点了取消 / 没有权限 / 这一端压根没有这个能力**。这两个接口给它们分家：

```c
char img[256];
if (ui_camera_capture(img, sizeof(img)) > 0) {
    ui_2d_image(0, 0, img, 320, 240);
} else if (ui_error_get_code() != VML_E_CANCELLED) {
    char why[64];
    ui_error_get_message(why, sizeof(why));   /* 人话，例如「没有权限」 */
    ui_dlg_msg("取图失败", why, VML_DLG_WARN);
}
```

| 值 | 名字 | 含义 |
|---|---|---|
| 0 | `VML_E_OK` | 没有错误（上一次调用成功了） |
| 1 | `VML_E_CANCELLED` | 用户取消了 —— **不是故障** |
| 2 | `VML_E_PERMISSION` | 没有权限（被拒 / "不再询问"） |
| 3 | `VML_E_UNSUPPORTED` | 这一端没有这个能力（没相机 / 没 GPS / 没这个传感器） |
| 4 | `VML_E_TIMEOUT` | 超时 |
| 5 | `VML_E_FAILED` | 其它失败（宿主内部错误 / 异常 / 文件不存在） |

### `ui_error_get_code(void)`
最近一次失败的错误码；**没有错误就是 0**。

⚠ 它描述的是「**刚才那一次**宿主调用」—— 中间再调任何别的 `ui_*`，槽就被清回 0，
所以要在**失败之后立刻**读。这两个读口**自己不清槽**，先问码再问消息读的是同一次失败。

```c
if (ui_gps_fetch(5000) == 0) {
    if (ui_error_get_code() == VML_E_PERMISSION) { /* 引导用户去系统设置里开权限 */ }
}
```

### `ui_error_get_message(char* buf, int cap)`
把**人话描述**写进 `buf`（最多 `cap` 字节，含结尾 0），返回写入的字节数；
没有错误就写空串、返回 0。`cap` 传 0 = **只问长度**（一个字节都不写）。
描述跟随**界面语言**（中文端中文、英文端英文），可以直接拿去给玩家看。

⚠ `VML_E_CANCELLED` 是玩家的正常操作，**别当故障终止程序**。
