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;
}