← 都来码 / Dolaima← 例程索引c/app/battery.c原文

c/app/battery.c · 34873 字节 · 下载原文

/* battery.c —— 常用小程序⑧:**电池与省电信息**(第 4 层:`ui_*` 图形界面)
 * battery.c -- Everyday utility #8: **battery and power-saving info** (layer 4: the `ui_*` interface)
 *
 * 干什么:一屏四张卡片。① 电量百分比 + **一条水平进度条**;② 是否在充电 / 是否处于省电模式;
 * What it does: four cards on one screen. (1) The battery percentage with **a horizontal bar**;
 * ③ 两项设备信息(屏幕尺寸、界面语言);④ 一句"该不该省电"的建议。
 * (2) whether it is charging and whether the system power-saver is on; (3) two pieces of device info
 * 颜色有讲究:**充电中绿、未充电蓝、电量低于 20% 红**(真彩 `0xAARRGGBB`)。
 * (screen size, UI language); (4) what to do about it. The colours carry meaning: **green while
 * 进度条与那个百分比**用的是同一个颜色真源**(`levelColor`)—— 两处各判一次就会出现
 * charging, blue while not, red below 20%** (true colour `0xAARRGGBB`). The bar and the percentage
 * "条是红的、字是蓝的"这种自相矛盾。
 * **share one colour source** (`levelColor`); judging twice gives the contradiction of a red bar with
 * blue text.
 *
 * ## ⚠ 手机上才有 —— 桌面(vmlcli)上"本机读不到电池信息"才是**正确输出**
 * ## ⚠ Phone-only -- on the desktop (vmlcli) "no battery information here" is the **correct output**
 *
 * 台式机没有电池,宿主**如实**报"没有":实测 `ui_battery_read()` 返回 0、`ui_battery_get_level()`
 * A desktop has no battery and the host **honestly** reports absence: measured, `ui_battery_read()`
 * 也返回 0。拿 **100 冒充满电**是这里最容易犯的错 —— 那会让程序以为"电很足",
 * returns 0 and so does `ui_battery_get_level()`. Faking **100 for "full"** is the easy mistake here --
 * 而真相是"这个问题在这台机器上没有意义"。所以本文件**先问"有没有"**,
 * it would make the program think there is plenty of charge when the truth is "this question is
 * 没有就说出来,并**照常把整个界面画完**(卡片、标题、轨道都在,只有数值位显示 `--`)。
 * meaningless on this machine". So this file **asks whether it has one first**, says so when it does
 * 判据是**看图就能分辨**:降级那张的进度条是空轨道 + 「--」,并有一行
 * not, and **draws the whole interface anyway** (cards, headings and the empty track are all there;
 * 「本机读不到电池信息(手机/笔记本上才有)」。
 * only the numbers read `--`). The criterion is visible in the PNG: the degraded one shows an empty
 * track, `--` and the line "no battery information on this device (phones and laptops only)".
 *
 * ## 桌面怎么看到"有电池"那一半:宿主自带的输入脚本注入,**不用改一行源码**
 * ## Seeing the battery branch on the desktop: the host's input-script injection, **no source change**
 *
 *     printf '0 battery 85 1\n0 saver 0\n' > in.txt
 *     vmlcli battery.c --vml-home third_party/vml --input in.txt --timeout 20 --frame battery.png
 *
 * `battery <电量 0-100> <是否充电 0|1>`、`saver <0|1>`(见 `CliVmlHost` 的输入脚本段)。
 * `battery <level 0-100> <charging 0|1>` and `saver <0|1>` (see the input-script section of
 * 换一组数(比如 `battery 12 0`)就得到一张"低电量警示色"的图。
 * `CliVmlHost`). Changing the numbers (say `battery 12 0`) gives the low-battery warning colour.
 *
 * ## 踩到的坑(都是这一轮真撞上的)
 * ## Pitfalls (all hit for real while writing this file)
 *
 * ① **`ui_power_read(VML_POWER_OP_SAVER, buf)` 不写缓冲区**:读宿主源码,
 * ① **`ui_power_read(VML_POWER_OP_SAVER, buf)` does not write the buffer**: the host's saver branch
 *   `POWER` 的 saver 分支直接 `registers[0] = PowerSaverOn() ? 1 : 0` —— **返回值就是状态本身**,
 *   is just `registers[0] = PowerSaverOn() ? 1 : 0` -- **the return value is the state itself**, not a
 *   不是"有效/无效"的标志,而且**一个字节都不往 `out` 里写**。
 *   valid/invalid flag, and it writes **not one byte** into `out`.
 *   同一个函数在 battery 那个 op 上却是"1 = 有效、0 = 没有电池,值写进 out[0]/out[1]" ——
 *   The same function on the battery op instead means "1 = valid, 0 = no battery, values in
 *   **同一个入口两种返回语义**,照 battery 的写法去读 saver 会拿到上一次的残留值。
 *   out[0]/out[1]" -- **one entry point with two return semantics**; reading the saver the way you read
 *   本文件因此对 saver 用**专用包装** `ui_power_is_saving()`(它就是同一个号、同一个操作码,
 *   the battery hands you a stale leftover. So this file uses the **dedicated wrapper**
 *   只是把语义钉死了),不碰那个通用入口。实测 `--input` 里给 `saver 1` 时它返回 1。
 *   `ui_power_is_saving()` for it (the same syscall and opcode, with the semantics nailed down) and
 *   leaves the generic entry alone. Measured with `saver 1` in `--input`: it returns 1.
 * ② **省电模式与"电量低"是两件事**:一个是系统策略、一个是电量事实。合成一句话显示,
 * ② **Power-save mode and "low battery" are two different things**: one is a system policy, the other
 *   就会出现"插着电却显示电量低"这种看着像 bug 的界面(`tilt.c` 里也专门记过这一条)。
 *   a fact about charge. Merging them into one line produces the bug-looking case of "plugged in yet
 *   shown as low" (`tilt.c` records the same rule).
 * ③ **读电量不用每帧**:电量变得很慢,每帧问一次是白白多走一趟 syscall。本文件设了
 * ③ **No need to read the battery every frame**: it changes very slowly, and asking every frame is a
 *   `BATT_POLL_TICKS` —— 每 20 拍(约 4 秒)问一次(见 `tick`)。
 *   wasted round trip through a syscall. This file has `BATT_POLL_TICKS`, asking every 20 ticks
 *   ("有没有电池"这个判断是例外,它要重问 —— 见 ④。)
 *   (about 4 seconds), see `tick`. (The "does it even have one" probe is the exception, see 4.)
 * ④ **桌面注入与程序启动之间有赛跑**:`--input` 的事件由另一个线程按时刻投,程序可能在第一拍就
 * ④ **There is a race between desktop injection and program start**: `--input` events are delivered by
 *   问了"有没有电池",那一瞬间还没注入 ⇒ 读到"没有"。所以**"没有"这个结论每拍都重问一次**
 *   another thread on a timer, and the program may ask in its very first tick, before the injection
 *   (一旦有了就转成 ③ 那个慢节奏)。
 *   lands, reading "none". So "none" is **re-asked every tick** (and once present it moves to the slow
 *   cadence of 3).
 * ⑤ **`ui_scr_set_keep_on` 是省电的另一半**:屏一直亮着是手机最大的耗电项,本程序在省电模式下
 * ⑤ **`ui_scr_set_keep_on` is the other half of power saving**: keeping the screen lit is a phone's
 *   把它关掉(见 `applyKeepOn`)。桌面上它只打一行日志(那是判据)。
 *   biggest drain, and this program turns it off while the saver is on (see `applyKeepOn`). On the
 *   desktop it only logs one line, which is the criterion.
 * ⑥⚠ **文件级 `static` 标量的初值会被静默丢掉**(2026-10-07 实测,本文件当场撞上):
 * ⑥⚠ **A file-scope `static` scalar silently loses its initializer** (measured 2026-10-07, hit right
 *   本文件原来写 `static int keepOnLast = -1;`,实测它**读到 0** —— 于是省电模式那一轮
 *   here). This file originally had `static int keepOnLast = -1;`, which **reads 0** -- so the saver
 *   (`want` 恰好是 0)**一次都不调 `ui_scr_set_keep_on`**、日志里一行都没有,而代码看着全对。
 *   run (`want` happens to be 0) made **no call to `ui_scr_set_keep_on`** and left no log line, while
 *   探针量出来的边界(`scripts/vmlcli` 跑 `T:\probe_static*.c` 那三个):
 *   the code looked entirely correct. The boundary, measured with three probe programs:
 *     · 文件级 `static int neg = -1;` → **0**;`= (0-1)` / `= -1` 经宏 → 也是 **0**;
 *     · file scope `static int neg = -1;` -> **0**; `= (0-1)` and a macro-wrapped `-1` -> **0** too;
 *     · 文件级**正**字面量(`= 7` / `= 100000`)→ 正常;
 *     · file scope with a **positive** literal (`= 7` / `= 100000`) -> fine;
 *     · **数组**初始化器(含负值)→ 正常;**函数内** `static` 的负初值 → 正常;
 *     · **array** initializers (negatives included) -> fine; a **function-scope** `static` negative -> fine;
 *     · 文件级 `static char buf[8] = "hi";` → **空串**(字符串初值也一起丢)。
 *     · file scope `static char buf[8] = "hi";` -> an **empty string** (a string initializer goes too).
 *   ⇒ 判据很简单:**文件级的 static 要么不给初值(默认 0),要么给一个字面正数**;
 *   ⇒ The rule is simple: **a file-scope static either gets no initializer (0 by default) or a positive
 *     要"哨兵值"就用一个不可能取到的**正数**(本文件用 2 —— `want` 只能是 0 或 1)。
 *     literal**; for a sentinel use a **positive** value that cannot occur (this file uses 2, since `want`
 *     is only ever 0 or 1).
 *
 * 版面:尺寸一律从宿主问(`ui_scr_get_w()/_h()`),颜色是真彩 `0xAARRGGBB`,画完必须 `ui_2d_present()`。
 * Layout: every size is asked of the host (`ui_scr_get_w()/_h()`), colours are true colour
 * 循环**必须有界**(例程要能被自动验收):空闲 10 秒自己退出,`Esc`/`q` 也退出,
 * `0xAARRGGBB`, and the frame ends with `ui_2d_present()`. The loop **must be bounded** (samples have to
 * 其余任意键 = 立刻重读一次。写真正的程序时把 `IDLE_EXIT_MS` 去掉即可。
 * be automatically verifiable): it exits after 10 s of idling, `Esc`/`q` quits, and any other key
 * re-reads at once. Drop `IDLE_EXIT_MS` when you write a real program.
 */

#include <vml_ui.h>
#include <stdlib.h>   /* itoa --- digits to text via the library */

#define IDLE_EXIT_MS     10000    /*  空闲 10 秒自己退出(验收用) */
                                  /* idle exit after 10 s (for automated checks) */
#define BATT_POLL_TICKS  20       /*  每 20 拍(200ms 一拍 ≈ 4 秒)问一次电量 */
                                  /* ask for the battery every 20 ticks (200 ms each, so about 4 s) */
#define LOW_LEVEL        20       /*  低于这个百分比就用警示色 */
                                  /* below this percentage the warning colour is used */

/* 配色(真彩 AA RRGGBB)*/
/* Colours (true colour AA RRGGBB) */
#define C_BG      0xFF0E1116
#define C_CARD    0xFF1A1F28
#define C_TRACK   0xFF2A3038
#define C_TITLE   0xFFE8E8F0
#define C_DIM     0xFF8A94A8
#define C_CHG     0xFF3FAE6A     /*  充电中(绿) */
                                 /* charging (green) */
#define C_DIS     0xFF4A90D9     /*  未充电(蓝) */
                                 /* discharging (blue) */
#define C_LOW     0xFFE05A4A     /*  电量偏低(红) */
                                 /* battery low (red) */
#define C_WARN    0xFFE0A040     /*  省电模式(琥珀) */
                                 /* power-saver mode (amber) */

/* ── 全局状态 ───────────────────────────────────────────────────────── */
/* -- Global state -- */
static int SW;                    /*  画布宽 */
                                 /* canvas width */
static int SH;                    /*  画布高 */
                                 /* canvas height */
static int g_pad;                 /*  页边距 */
                                 /* page margin */
static int lang;                  /*  界面语言:0 = 中文,1 = English */
                                 /* UI language: 0 = Chinese, 1 = English */

/* 电量那两个值**放一个数组里**(`ui_battery_read` 一次写两格),单独一行一条声明。
 * The two battery values live in **one array** (one `ui_battery_read` writes both), one declaration
 * 单独的元素(`battSaver`)另放:它走的是另一个操作码,见文件头坑 ①。
 * per line. The saver flag is separate because it comes from a different opcode, see pitfall 1.
 */
static int bat[2];                /*  bat[0] 电量 0-100、bat[1] 充电中 1/0 */
                                 /* bat[0] level 0-100, bat[1] charging 1/0 */
static int hasBat;                /*  这台机器有电池吗(0 = **没有**,不是"电量 0%")*/
                                 /* does this machine have a battery (0 = **none**, not "0 percent") */
static int battSaver;             /*  省电模式 */
                                 /* power-saver mode */

/* 卡片的纵向排布(画与命中都不用坐标,只是四张卡的位置)*/
/* The vertical card layout (nothing is hit-tested; these are just the four card positions) */
static int cardX;
static int cardW;
static int cardY[4];
static int cardH[4];

/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */

/* 整数 → 十进制串(`ui_2d_text` 只收字符串)。
 * Integer to a decimal string (`ui_2d_text` takes strings only).
 * ⚠ 返回**同一个静态缓冲**,别在一个表达式里连用两次。
 * ⚠ It returns **one static buffer**; never use it twice in one expression.
 */
static char g_num[16];
static char* numstr(int v) {
    itoa(v, g_num);
    return g_num;
}

static int clampInt(int v, int lo, int hi) {
    if (v < lo) return lo;
    if (v > hi) return hi;
    return v;
}

/* 电量该用什么颜色 —— **全文件唯一的颜色真源**:进度条与那个大百分比都读它。
 * The colour for a battery level -- **the single colour source in this file**: both the bar and the big
 * ⚠ 优先级写死在这里:**低于 20% 一律警示色**(哪怕在充电 —— 插着电还低说明充得很慢,
 * percentage read it. ⚠ The precedence lives here: **below 20% always warns** (charging or not -- low
 *   仍然值得提醒);其次才看充电。
 * while plugged in means it is charging slowly, which is still worth flagging); charging comes second.
 */
static int levelColor(int level, int charging) {
    if (level < LOW_LEVEL) return C_LOW;
    if (charging != 0) return C_CHG;
    return C_DIS;
}

/* ── 读电量 ─────────────────────────────────────────────────────────── */
/* -- Reading the battery -- */

/* 采一次。返回 1 = 画面要重画。
 * Samples once. Returns 1 when the screen needs a redraw.
 */
static int poll(void) {
    int ok;
    int changed;
    int old0;
    int old1;
    int oldH;
    int oldS;

    old0 = bat[0];
    old1 = bat[1];
    oldH = hasBat;
    oldS = battSaver;
    changed = 0;

    /* ⚠ 返回值是**两回事**:1 = 有效、0 = **这台机器没有电池**(不是"电量 0%")。
     * ⚠ The return value means two different things: 1 = valid, 0 = **this machine has no battery**
     *   把 0 当"电量 0%"用,界面就会在台式机上显示一个红的空电池 —— 那是撒谎。
     *   (not "0 percent"). Treating 0 as "0 percent" would show a red empty battery on a desktop --
     *   a lie.
     */
    ok = ui_battery_read(bat);
    if (ok == 1) hasBat = 1;
    else { hasBat = 0; bat[0] = 0; bat[1] = 0; }

    /* ⚠ 省电模式**不在** `ui_battery_read` 里,它是另一个操作码 —— 忘了这一句 `battSaver`
     * ⚠ The power-saver flag is **not part of** `ui_battery_read`; it is a separate opcode. Forget
     *   永远 0,而代码看着完全正常("省电模式"那一行就是不显示,也不报错)。
     *   this and `battSaver` stays 0 while the code looks perfectly fine (the saver row simply never
     *   见文件头坑 ①:**用专用包装**读它,别拿通用入口。
     *   lights up, with no error). See pitfall 1: read it through the **dedicated wrapper**, not the
     *   generic entry.
     */
    battSaver = ui_power_is_saving();

    if (bat[0] != old0 || bat[1] != old1 || hasBat != oldH || battSaver != oldS) changed = 1;
    return changed;
}

/* 屏常亮 = 电量最大的开销之一:省电模式下关掉它。桌面只打一行日志(那是判据)。
 * Keeping the screen on is one of the biggest drains: turn it off while the saver is on. On the desktop
 * ⚠ 只在**变化时**调:每拍调一次会把日志刷屏,也就不再是判据了。
 * it only logs a line (the criterion). ⚠ Only call it **on change**: calling every tick would flood the
 * log and stop being a criterion.
 * ⚠⚠ **初值写 2,不能写 -1**:文件级 `static int x = -1;` 的负初值会被**静默丢掉、变成 0**
 * ⚠⚠ **The initial value is 2, and must not be -1**: a file-scope `static int x = -1;` silently loses
 *   (见文件头坑 ⑥)。写成 -1 时,第一次调用恰好赶上 `want == 0`(省电模式开着)就**一次都不调**,
 *   its negative initializer and becomes 0 (pitfall 6). With -1, a first call that happens to want 0
 *   而文件里一行日志都没有 —— 实测:`saver 1` 那一轮就是既不关屏、也没有任何提示。
 *   (saver already on) makes **no call at all** and leaves no log line -- measured: with `saver 1` the
 *   `want` 只可能是 0 或 1,所以 2 这个初值保证"第一次一定不同"。
 *   program neither turned the screen off nor said anything. `want` can only be 0 or 1, so 2 guarantees
 *   the first call always looks like a change.
 */
static int keepOnLast = 2;
static void applyKeepOn(void) {
    int want;
    want = (battSaver != 0) ? 0 : 1;
    if (want != keepOnLast) {
        ui_scr_set_keep_on(want);
        keepOnLast = want;
    }
}

/* ── 版面 ───────────────────────────────────────────────────────────── */
/* -- Layout -- */

/* 四张卡按权重分高度:电量那张最大(它有大字和进度条),建议那张最小。
 * The four cards split the height by weight: the battery one is largest (it holds big digits and the
 * ⚠ 权重是**画在一个数组里**的,不是四个魔数散在代码里 —— 加一张卡只加一个数。
 * bar) and the advice one is smallest. ⚠ The weights live **in one array** rather than as four magic
 * numbers scattered through the code: adding a card adds one number.
 */
static int weight[4] = { 5, 3, 3, 2 };

static void layout(void) {
    int top;
    int foot;
    int gap;
    int avail;
    int total;
    int i;
    int used;

    SW = ui_scr_get_w();
    SH = ui_scr_get_h();
    if (SW <= 0) SW = 360;
    if (SH <= 0) SH = 620;

    g_pad = SW / 18;
    if (g_pad < 10) g_pad = 10;

    cardX = g_pad;
    cardW = SW - g_pad * 2;
    top = g_pad + 46;                       /*  标题两行之下 */
                                           /* below the two heading lines */
    foot = SH - 34;                         /*  底部提示行 */
                                           /* the footer line */
    gap = 10;
    avail = foot - top - gap * 3;
    if (avail < 120) avail = 120;

    total = 0;
    for (i = 0; i < 4; i++) total = total + weight[i];
    used = 0;
    for (i = 0; i < 4; i++) {
        cardY[i] = top + used;
        /* ⚠ **最后一张卡的高度由"到提示行为止还剩多少"决定**,不是"总高减去前面用掉的" ——
         * ⚠ **The last card's height comes from "how much is left above the footer"**, not from
         *   矮屏上前三张被最小高度顶大之后,"剩下的"会算成**负数**,卡片就会盖住底部提示行
         *   "total minus what the first three used": on a short screen the minimum heights push the
         *   (实测 640x360:末张卡压在"任意键立刻重读"那一行上)。按 `foot` 反推则永远不越界。
         *   first three up, the remainder goes **negative** and the card covers the footer (measured at
         *   640x360: the last card sat on the "any key re-reads" line). Anchoring to `foot` cannot
         *   overflow.
         */
        if (i == 3) cardH[i] = foot - cardY[i];
        else {
            cardH[i] = avail * weight[i] / total;
            if (cardH[i] < (i == 0 ? 88 : 46)) cardH[i] = (i == 0 ? 88 : 46);
        }
        used = used + cardH[i] + gap;
    }
}

/* ── 绘制 ───────────────────────────────────────────────────────────── */
/* -- Drawing -- */

/* 一张卡:圆角底 + 一圈淡边 */
/* One card: a rounded fill plus a faint outline */
static void drawCard(int y, int h) {
    ui_2d_rect(cardX, y, cardW, h, C_CARD, 1, 0, 12);
}

/* 标题 + 值,一行;值右对齐(信息卡与状态卡都用它,省得两处各写一遍排布)
 * A label plus a value on one row, the value right-aligned (used by both the info and the status card,
 * so the row layout is written once)
 */
static void drawRow(int y, char* label, char* value, int vcol) {
    ui_2d_text(cardX + 14, y, label, C_DIM, 13, VML_ANCHOR_LEFT);
    ui_2d_text(cardX + cardW - 14, y, value, vcol, 14, VML_ANCHOR_RIGHT);
}

/* 水平进度条:先画整条轨道,再画"已用"那一段。
 * The horizontal bar: the full track first, then the "used" segment on top.
 * ⚠ 电量读不到时**照样画轨道**(空条),只是不填 —— 界面完整、也一眼看出"是空的"。
 * ⚠ When the level cannot be read the track is **still drawn** (empty) and simply not filled: the screen
 * stays complete and "empty" is obvious at a glance.
 */
static void drawBar(int x, int y, int w, int h, int level, int charging) {
    int fw;
    int r;
    r = h / 2;
    ui_2d_rect(x, y, w, h, C_TRACK, 1, 0, r);
    if (hasBat == 0) return;
    fw = w * clampInt(level, 0, 100) / 100;
    if (fw < 4 && level > 0) fw = 4;        /*  1% 也要看得见一格 */
                                           /* even one percent gets a visible sliver */
    ui_2d_rect(x, y, fw, h, levelColor(level, charging), 1, 0, r);
}

/* 卡①:电量百分比 + 进度条。**从上往下按卡片高度算**(大字居中、状态行、进度条贴底)——
 * Card 1: the battery percentage plus the bar. **Laid out top-down from the card height** (big digits
 * 三项都从 `h` 推出来,所以矮屏上不会叠在一起(实测第一版按 `y + h/2 ± 常数` 摆,
 * centred, then the status line, then the bar at the bottom), so they cannot overlap on a short screen.
 * 640x360 时状态行正好压在进度条上)。
 * (The first version used `y + h/2 +/- constants`, measured to put the status line right on top of the
 * bar at 640x360.)
 */
static void drawBatteryCard(void) {
    int y;
    int h;
    int nSize;
    int barH;
    int barY;
    int statY;
    int pctY;
    int topLim;
    char buf[32];

    y = cardY[0];
    h = cardH[0];
    drawCard(y, h);

    barH = 16;
    barY = y + h - 14 - barH;
    /* 字号跟着卡片高度走:大字在矮卡里会把别人挤掉 */
    /* The font size follows the card height: big digits would squeeze everything else out of a short card */
    if (h >= 150) nSize = 34;
    else if (h >= 118) nSize = 26;
    else nSize = 20;
    /* 卡片矮到放不下"大字 + 状态行 + 进度条"三样时**省掉状态行** —— 信息没丢,
     * When the card is too short for all three (digits, status line, bar) the status line is **dropped**;
     * 卡②本来就写着充电状态,而两行叠在一起才是真难看。
     * nothing is lost (card 2 states the charging status anyway) and overlapping text is what really looks
     * bad.
     */
    statY = (h >= 104) ? (barY - 20) : 0 - 1;
    topLim = (statY >= 0) ? (statY - 4) : (barY - 6);
    pctY = y + 28 + (topLim - (y + 28) - nSize) / 2;
    if (pctY < y + 26) pctY = y + 26;

    ui_2d_text(cardX + 14, y + 10, lang == 0 ? "电量" : "Battery", C_DIM, 13, VML_ANCHOR_LEFT);

    if (hasBat == 0) {
        /* ⚠ **降级分支**:明说"读不到",数值位给 `--`,**不画填充**。
         * ⚠ **The degraded branch**: say plainly that there is nothing to read, show `--` and **fill
         *   拿 100 冒充满电是这里最容易犯的错(见文件头)。
         * nothing**. Faking 100 for "full" is the easy mistake here (see the header).
         */
        ui_2d_text(SW / 2, pctY, "--", C_DIM, nSize, VML_ANCHOR_CENTER);
        /* 窄屏上这句话比画布还宽 ⇒ 拆成两行(照样是"明说",只是折了一下)*/
        /* On a narrow screen this sentence is wider than the canvas => split it in two (still said
           plainly, just wrapped) */
        if (SW < 400) {
            ui_2d_text(SW / 2, statY, lang == 0 ? "本机读不到电池信息" : "no battery information here",
                       C_WARN, 12, VML_ANCHOR_CENTER);
            ui_2d_text(SW / 2, statY + 16, lang == 0 ? "(手机/笔记本上才有)" : "(phones and laptops only)",
                       C_WARN, 12, VML_ANCHOR_CENTER);
        } else {
            ui_2d_text(SW / 2, statY,
                       lang == 0 ? "本机读不到电池信息(手机/笔记本上才有)"
                                 : "no battery information here (phones and laptops only)",
                       C_WARN, 12, VML_ANCHOR_CENTER);
        }
    } else {
        buf[0] = 0;
        strcat(buf, numstr(clampInt(bat[0], 0, 100)));
        strcat(buf, "%");
        /* 颜色与进度条**同源**(`levelColor`)—— 见文件头 */
        /* The colour comes from the **same source** as the bar (`levelColor`), see the header */
        ui_2d_text_styled(SW / 2, pctY, buf, levelColor(bat[0], bat[1]), nSize, VML_ANCHOR_CENTER, VML_FONT_BOLD);
        if (statY >= 0) {
            if (bat[1] != 0 && bat[0] >= LOW_LEVEL) {
                ui_2d_text(SW / 2, statY, lang == 0 ? "正在充电" : "charging",
                           C_CHG, 13, VML_ANCHOR_CENTER);
            } else if (bat[0] < LOW_LEVEL && bat[1] == 0) {
                ui_2d_text(SW / 2, statY, lang == 0 ? "电量偏低,该省电了" : "low - time to save power",
                           C_LOW, 13, VML_ANCHOR_CENTER);
            } else {
                ui_2d_text(SW / 2, statY, lang == 0 ? "未充电" : "on battery",
                           C_DIM, 13, VML_ANCHOR_CENTER);
            }
        }
    }

    drawBar(cardX + 14, barY, cardW - 28, barH,
            hasBat == 1 ? bat[0] : 0, hasBat == 1 ? bat[1] : 0);
}

/* 卡②:充电 / 省电(**两件事分开写**,见文件头坑 ②)*/
/* Card 2: charging and power saving (**two separate rows**, see pitfall 2) */
static void drawStatusCard(void) {
    int y;
    int h;
    int ry;
    char buf[24];

    y = cardY[1];
    h = cardH[1];
    drawCard(y, h);
    ry = y + h / 2 - 20;

    ui_2d_text(cardX + 14, ry, lang == 0 ? "充电状态" : "Charging", C_DIM, 13, VML_ANCHOR_LEFT);
    if (hasBat == 0) {
        ui_2d_text(cardX + cardW - 14, ry, "--", C_DIM, 14, VML_ANCHOR_RIGHT);
    } else if (bat[1] != 0) {
        ui_2d_text(cardX + cardW - 14, ry, lang == 0 ? "充电中" : "yes", C_CHG, 14, VML_ANCHOR_RIGHT);
    } else {
        ui_2d_text(cardX + cardW - 14, ry, lang == 0 ? "未充电" : "no", C_DIS, 14, VML_ANCHOR_RIGHT);
    }

    ui_2d_text(cardX + 14, ry + 26, lang == 0 ? "省电模式" : "Power saver", C_DIM, 13, VML_ANCHOR_LEFT);
    buf[0] = 0;
    strcat(buf, battSaver != 0 ? (lang == 0 ? "已开启" : "on") : (lang == 0 ? "已关闭" : "off"));
    ui_2d_text(cardX + cardW - 14, ry + 26, buf,
               battSaver != 0 ? C_WARN : C_DIM, 14, VML_ANCHOR_RIGHT);
}

/* 卡③:设备信息(屏幕尺寸与语言都从宿主问)*/
/* Card 3: device info (screen size and language both come from the host) */
static void drawInfoCard(void) {
    int y;
    int h;
    int ry;
    char buf[32];

    y = cardY[2];
    h = cardH[2];
    drawCard(y, h);
    ry = y + h / 2 - 20;

    buf[0] = 0;
    strcat(buf, numstr(SW));
    strcat(buf, " x ");
    strcat(buf, numstr(SH));
    drawRow(ry, lang == 0 ? "屏幕(像素)" : "Screen (px)", buf, C_TITLE);

    drawRow(ry + 26, lang == 0 ? "界面语言" : "UI language",
            lang == 0 ? "中文" : "English", C_TITLE);
}

/* 卡④:一句建议 —— 光有读数没用,**读它是为了做决定**
 * Card 4: a piece of advice -- readings alone are useless; **you read them in order to decide**
 */
static void drawAdviceCard(void) {
    int y;
    int h;
    char* l1;
    char* l2;

    y = cardY[3];
    h = cardH[3];
    drawCard(y, h);

    if (hasBat == 0) {
        l1 = lang == 0 ? "没有电池可读" : "no battery to read";
        l2 = lang == 0 ? "省电建议在这台机器上无从谈起" : "power advice is moot on this machine";
    } else if (bat[0] < LOW_LEVEL && bat[1] == 0) {
        l1 = lang == 0 ? "电量偏低且没在充电" : "low and not charging";
        l2 = lang == 0 ? "该省了:少画点、关音效、提醒存档" : "save power: draw less, mute, prompt to save";
    } else if (battSaver != 0) {
        l1 = lang == 0 ? "系统已开省电模式" : "system power saver is on";
        l2 = lang == 0 ? "程序跟着降档:关掉屏常亮" : "follow suit: screen no longer kept on";
    } else {
        l1 = lang == 0 ? "电量充裕" : "plenty of charge";
        l2 = lang == 0 ? "等电量低了再降画质就够了" : "reserve the downgrade for a low battery";
    }

    ui_2d_text(cardX + 14, y + h / 2 - 18, l1, C_TITLE, 13, VML_ANCHOR_LEFT);
    ui_2d_text(cardX + 14, y + h / 2 + 4, l2, C_DIM, 12, VML_ANCHOR_LEFT);
}

static void draw(void) {
    ui_2d_clear(C_BG);

    ui_2d_text_styled(SW / 2, g_pad, lang == 0 ? "电池与省电" : "Battery and Power",
                      C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD);
    if (hasBat == 1) {
        ui_2d_text(SW / 2, g_pad + 26, lang == 0 ? "数据源 ui_battery_read" : "source ui_battery_read",
                   C_DIM, 11, VML_ANCHOR_CENTER);
    } else {
        ui_2d_text(SW / 2, g_pad + 26, lang == 0 ? "本机没有电池(桌面/台式机)" : "no battery on this device",
                   C_WARN, 11, VML_ANCHOR_CENTER);
    }

    drawBatteryCard();
    drawStatusCard();
    drawInfoCard();
    drawAdviceCard();

    ui_2d_text(SW / 2, SH - 24,
               lang == 0 ? "任意键立刻重读   Esc / q 退出" : "any key re-reads   Esc / q quits",
               C_DIM, 11, VML_ANCHOR_CENTER);

    ui_2d_present();      /* ⚠ 不调这句 = 黑屏 */
                          /* without this call you get a black screen */
}

/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
    int msg[4];
    int t;
    int t0;
    int done;
    int tick;

    lang = ui_scr_get_language();   /*  开局查一次,别每帧调 */
                                   /* queried once, not every frame */

    poll();                         /*  首帧之前先读一次,否则第一帧画的是"没有电池" */
                                   /* read once before the first frame, or it renders as "no battery" */

    layout();
    ui_win_open_ex(lang == 0 ? "电池与省电" : "Battery and Power",
                   0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
    layout();
    applyKeepOn();                  /*  屏常亮是**窗口**的属性 ⇒ 开窗之后再设(照 tilt.c)*/
                                   /* keeping the screen on is a property of the **window** => set it after opening one */
    draw();

    tick = 0;
    t0 = ui_time_get_ms();
    done = 0;
    while (!done && ui_win_is_closed() == 0) {
        t = ui_msg_wait(msg, 200);
        if (t == VML_MSG_NONE) {
            if (ui_time_get_ms() - t0 > IDLE_EXIT_MS) { done = 1; continue; }
            tick = tick + 1;
            /* 电量变得很慢 ⇒ 不必每拍问(见文件头坑 ③);
             * The battery changes very slowly, so there is no need to ask every tick (pitfall 3);
             * 但**还没有答案时每拍重试** —— 桌面注入与启动之间有赛跑,见坑 ④。
             * but **retry every tick while there is no answer yet** -- the injection race, pitfall 4.
             */
            if (tick % BATT_POLL_TICKS == 0 || hasBat == 0) {
                if (poll() != 0) { layout(); applyKeepOn(); draw(); }
            }
            continue;
        }
        t0 = ui_time_get_ms();
        if (t == VML_MSG_KEYDOWN) {
            int k = msg[1];
            if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') done = 1;
            else {                       /*  任意键 = 立刻重读一次 */
                                        /* any other key re-reads at once */
                poll();
                layout();
                applyKeepOn();
                draw();
            }
            continue;
        }
        if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
            poll();
            applyKeepOn();
            draw();
            continue;
        }
        if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
            layout();
            draw();
            continue;
        }
        if (t == VML_MSG_WINDOWCLOSE) done = 1;
    }
    return 0;
}