c/app/stopwatch.c · 30232 字节 · 下载原文
/* stopwatch.c —— 常用小程序⑥:**秒表 / 倒计时**(第 4 层:`ui_*` 图形界面)
* stopwatch.c -- Everyday utility #6: a **stopwatch / countdown** (layer 4: the `ui_*` graphical UI)
*
* 干什么:一扇窗口两种模式,顶部两个页签切换。
* What it does: one window, two modes, switched by the two tabs at the top.
* · **秒表**:开始 / 暂停 / 清零,还可以**记圈**(下面列出每一圈的时间)。
* · **Stopwatch**: start / pause / reset, plus **laps** (each lap's time is listed below).
* · **倒计时**:1 / 3 / 5 分钟三个预设,也能 −10 秒 / +10 秒自己调;到点会停住并弹一行提示。
* · **Countdown**: three presets (1 / 3 / 5 minutes) plus -10 s / +10 s; at zero it stops and says so.
* 大字显示 `分:秒.百分秒`,自己格式化 —— `ui_2d_text` **只吃字符串**,第 4 层没有 `print_int`。
* The big readout is `MM:SS.hh`, formatted by hand -- `ui_2d_text` takes **strings only**, and layer 4
* has no `print_int`.
*
* 操作(触摸与键盘两条路都在)
* Controls (touch and keyboard both work)
*
* · 触摸:顶上两个页签切模式;中间那根大按钮 = 开始 / 暂停;下面一排 = 记圈 / 清零 / 改时长。
* · Touch: the two tabs switch modes; the big button starts and pauses; the row below does lap, reset
* and the duration.
* · 键盘:`空格` 开始 / 暂停 · `M` 切模式 · `L` 记圈 · `R` 清零 · `←`/`→` 各 ±10 秒 · `Esc`/`q` 退出。
* · Keyboard: Space starts/pauses, `M` switches mode, `L` laps, `R` resets, Left/Right shift ten
* seconds, and `Esc`/`q` quits.
*
* 时间怎么算 —— **`ui_time_get_ms()` 的差值,绝不"每帧加一个固定值"**
* How the time is computed -- **a difference of `ui_time_get_ms()`**, never a fixed step per frame
*
* 本仓的规矩(E162):**帧率不是时间**。宿主一改帧率,"每帧加 16 毫秒"这类计时立刻就不准,
* The repo rule (E162): **the frame rate is not time**. Change the host's pacing and a "add 16 ms per
* 而屏幕上看不出任何异常 —— 只有对着真秒表比才看得出来。
* frame" clock drifts immediately, with nothing visibly wrong on screen.
* 所以这里只保存两个数:已经累计的毫秒 `acc`、本次启动的时刻 `t0`。
* So only two numbers are kept here: the milliseconds accumulated so far (`acc`) and when the current
* run started (`t0`).
* 任何时刻的读数都是 `acc + (现在 - t0)` —— 暂停就是把 `acc` 定住、把 `t0` 丢掉。
* Any reading is `acc + (now - t0)`; pausing just freezes `acc` and drops `t0`.
*
* 到点的提示(**桌面无效就无效,绝不显示"失败"**)
* The alert at zero (**harmless when the desktop ignores it, and never shown as a failure**)
*
* `ui_tone_on` 起一个音、`ui_vibrate_simple` 震一下。桌面宿主上没有声卡和马达 ——
* `ui_tone_on` starts a tone and `ui_vibrate_simple` buzzes. The desktop host has no sound card and no
* 实测指令行宿主里 `tone_on` 回 -1、`vibrate` 回 0;手机上是好的。
* motor: measured in the command-line host, `tone_on` returns -1 and `vibrate` returns 0. On a phone
* **所以这两个返回值一律丢掉**(写进变量会诱使人去判它、进而把结果画到界面上,
* both work. So **both return values are dropped on the floor**; keeping them would tempt someone into
* 而桌面端那行字看着就像"坏了")。调用本身不影响任何正确性。
* testing them and painting the outcome on screen, which would make the desktop look broken.
*
* 踩过的坑(实测)
* Pitfalls (measured)
*
* ① `VML_KEY_LEFT/RIGHT` = 37/39、`VML_KEY_BACKSPACE` = 8 —— 都在可打印区间附近,
* ① `VML_KEY_LEFT/RIGHT` are 37/39 and `VML_KEY_BACKSPACE` is 8 -- all near the printable range, so
* 判按键时**具名键必须排在"可打印字符"之前**。
* named keys **must come before the printable range**.
* ② `ui_2d_text` 的 `y` 是**基线**(墨迹在 y 的**上方**)。按"y 是盒顶"排,
* ② The `y` of `ui_2d_text` is the **baseline** (ink sits **above** y). Lay out as if y were the top
* 大字会整体下沉,下沿被按钮压住。居中放一行字的算式是 `y_底 = 盒中心 + 字高 × 0.35`。
* and the big digits sink until the buttons cover their lower edge; the centring formula is
* `baseline = box_center + size * 0.35`.
* ③ **画与命中判定共用同一套几何**(`tab_rect` / `btn_rect`)。各算一套就会"看着点的是暂停、
* ③ **Drawing and hit testing share one set of geometry** (`tab_rect` / `btn_rect`); two copies and
* 实际按到了清零"。
* the pause you aimed at turns out to be the reset you pressed.
* ④ `ui_2d_text` 只收**字符串**:`分:秒.百分秒` 得自己拼(**逐字节写数字**,
* ④ `ui_2d_text` takes **strings**: `MM:SS.hh` has to be built by hand. Writing the digits a byte at a
* 一位一个字符没问题 —— 危险的是**往缓冲里逐字节写汉字**,一个汉字 3 个字节)。
* time is fine; what is dangerous is writing a CJK glyph byte by byte, since one glyph is 3 bytes.
* ⑤ 空闲 10 秒也退出(例程要能被自动验收)。**跑的时候也不重置这个计时** ——
* ⑤ Ten idle seconds also quit (samples must be verifiable automatically). A running clock does **not**
* 否则"正在计时"的程序永远不退出,自动验收就得靠 `--timeout` 硬砍。
* reset that clock; otherwise a running program never ends and the check has to hard-kill it.
*
* 跑法(桌面脚手架,秒级,别为改一行去打 APK)
* How to run (desktop scaffolding, seconds -- do not build an APK just to change one line)
*
* dotnet scripts/vmlcli/bin/Release/net10.0/vmlcli.dll Examples/c/app/stopwatch.c \
* --vml-home third_party/vml --timeout 40 --frame /tmp/stopwatch.png
* 手机上:命令行页敲 `vml run examples/c/app/stopwatch.c`。
* On the phone: type `vml run examples/c/app/stopwatch.c` in the command-line page.
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
/* 空闲这么久就自己退出(例程要能被自动验收)*/
/* Idle this long and it quits, so the sample can be verified. */
#define IDLE_EXIT_MS 10000
/* 记圈最多留几条 / 一次 ±10 秒 / 倒计时的上下限 */
/* How many laps are kept, the ten-second step, and the countdown's floor and ceiling. */
#define MAXLAP 6
#define STEP_MS 10000
#define CD_MIN 10000
#define CD_MAX 5990000
/* 预设的三个时长(1 / 3 / 5 分钟)*/
/* The three presets: one, three and five minutes. */
#define CD_1M 60000
#define CD_3M 180000
#define CD_5M 300000
/* 配色(真彩 AARRGGBB,第 4 层没有索引色)*/
/* Colours (true colour AARRGGBB; layer 4 has no palette). */
#define C_BG 0xFF0E1116
#define C_CARD 0xFF1A1F28
#define C_BTN 0xFF2A3340
#define C_TABON 0xFF4A90D9
#define C_TITLE 0xFFE8E8F0
#define C_DIM 0xFF9AA0B0
#define C_RUN 0xFF3FAE6A
#define C_PAUSE 0xFFFFC64A
#define C_ALARM 0xFFE05A4A
#define C_INK 0xFF101018
/* 后四个是状态色:计时中 = 绿、暂停 = 琥珀、到点 = 红,`C_INK` 是亮底按钮上的深色字 */
/* The last four are state colours: running green, paused amber, finished red; `C_INK` is dark ink on a bright button. */
/* ── 状态 ───────────────────────────────────────────────────────────── */
/* -- State -- */
/* 版面:全部由宿主给的尺寸算出来(**不写死坐标**,见文件头)*/
/* Layout: every value comes from the size the host reports, never hard-coded. */
static int SW;
static int SH;
static int lang;
static int pad;
static int fs;
static int big_fs;
static int tab_y, tab_h;
static int big_y;
static int status_y;
static int lap_top;
static int pre_y, pre_h;
static int main_y, main_h;
static int aux_y, aux_h;
static int hint_y;
/* `fs` 是小字、`big_fs` 是大字读数;下面四组 y 是四排按钮的位置 */
/* `fs` is the small font and `big_fs` the big readout; the four y values place the four button rows. */
static int mode;
static int running;
static int acc;
static int t0;
static int limit;
static int fired;
static int lap_ms[MAXLAP];
static int lap_n;
/* `acc` + `(现在 - t0)` 才是读数(见文件头"时间怎么算");`fired` = 倒计时已经到过点 */
/* The reading is `acc + (now - t0)` (see the header); `fired` means the countdown already hit zero. */
/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */
/* 整数 → 十进制串。返回**同一个静态缓冲**,别在一个表达式里连用两次。
* Integer to a decimal string. Returns **one static buffer**; never use it twice in one expression. */
static char numbuf[16];
static char *numstr(int v) {
itoa(v, numbuf);
return numbuf;
}
/* 毫秒 → `MM:SS.hh` 写进 `d`(要 9 个字节)。
* Milliseconds to `MM:SS.hh`, written into `d` (nine bytes needed).
* ⚠ **写字进缓冲,不返回静态缓冲** —— 静态的那份一被复用,同一次绘制里两处读数就会串。
* ⚠ It **writes into a buffer instead of returning a static one**: one shared static would make two
* readouts in the same frame overwrite each other.
* 封顶 99:59.99(再大显示就不下了)
* capped at 99:59.99, beyond which it no longer fits.
*/
static void fmt_into(char *d, int ms) {
int mm;
int ss;
int hh;
if (ms < 0) ms = 0;
if (ms > 5999990) ms = 5999990;
mm = ms / 60000;
ss = (ms / 1000) % 60;
hh = (ms / 10) % 100;
d[0] = (char)('0' + (mm / 10) % 10);
d[1] = (char)('0' + mm % 10);
d[2] = ':';
d[3] = (char)('0' + ss / 10);
d[4] = (char)('0' + ss % 10);
d[5] = '.';
d[6] = (char)('0' + hh / 10);
d[7] = (char)('0' + hh % 10);
d[8] = 0;
}
/* 已经过了多少毫秒(秒表算的就是它,倒计时算的是"上限减它")
* How many milliseconds have elapsed (what the stopwatch shows, and what the countdown subtracts). */
static int elapsed(void) {
int v = acc;
if (running) v = v + (ui_time_get_ms() - t0);
return v;
}
/* 屏幕上该显示的那个数 */
/* The number the screen should show */
static int reading(void) {
int e = elapsed();
if (mode == 0) return e;
if (e > limit) e = limit;
return limit - e;
}
/* ── 提醒 ───────────────────────────────────────────────────────────── */
/* -- The alert -- */
/* 到点的提醒:**试一下**发声与震动,结果**一律丢掉**(见文件头"到点的提示"那一段)。
* The alert at zero: **try** a tone and a vibration and **drop both results** (see the header).
* ⚠ 不要写成 `if (ui_tone_on(...) == 0) ...` —— 那会把"桌面没有声卡"变成界面上的一句话。
* ⚠ Never write `if (ui_tone_on(...) == 0) ...`: that turns "no sound card here" into on-screen text.
*/
static void chime(void) {
ui_tone_on(0, 84, 110);
ui_vibrate_simple(300, 200);
}
/* ── 动作 ───────────────────────────────────────────────────────────── */
/* -- Actions -- */
static void reset_all(void) {
acc = 0;
running = 0;
fired = 0;
lap_n = 0;
}
static void set_mode(int m) {
if (m == mode) return;
mode = m;
reset_all();
print_str(m == 0 ? "[stopwatch] mode=stopwatch\n" : "[stopwatch] mode=countdown\n");
}
static void start_stop(void) {
if (running) {
acc = elapsed(); /* 定住累计值,丢掉起点 */
/* freeze the total and drop the origin */
running = 0;
print_str("[stopwatch] pause at ");
print_int(acc);
print_str(" ms\n");
return;
}
if (mode == 1 && fired) reset_all(); /* 到点之后再按 = 重来 */
/* pressing again after zero restarts */
if (mode == 1 && acc >= limit) reset_all();
running = 1;
t0 = ui_time_get_ms();
print_str("[stopwatch] start ");
print_int(mode);
print_str(" at ");
print_int(t0);
print_str(" ms\n");
}
static void lap(void) {
int i;
if (mode != 0) return;
i = elapsed();
if (i <= 0) return;
if (lap_n < MAXLAP) {
lap_ms[lap_n] = i;
lap_n = lap_n + 1;
} else {
/* 满了就把最老的一圈挤掉(新的一圈才是用户想看的)*/
/* When full the oldest lap is pushed out (the new one is what the user asked for) */
i = 0;
while (i < MAXLAP - 1) { lap_ms[i] = lap_ms[i + 1]; i = i + 1; }
lap_ms[MAXLAP - 1] = elapsed();
}
print_str("[stopwatch] lap ");
print_int(lap_n);
print_str(" = ");
print_int(elapsed());
print_str(" ms\n");
}
static void adjust(int d) {
if (mode != 1 || running) return;
limit = limit + d;
if (limit < CD_MIN) limit = CD_MIN;
if (limit > CD_MAX) limit = CD_MAX;
acc = 0;
fired = 0;
print_str("[stopwatch] limit = ");
print_int(limit);
print_str(" ms\n");
}
static void preset(int v) {
if (mode != 1) return;
limit = v;
reset_all();
print_str("[stopwatch] preset = ");
print_int(v);
print_str(" ms\n");
}
/* 每次画之前推一次:倒计时到点就停住、提醒一声(**只做一次**,靠 `running` 归零兜住)。
* Advanced once before every draw: the countdown stops at zero and buzzes (only once, since `running`
* going to zero is what gates it).
* ⚠ 判据是"已经过了多少 ≥ 上限",不是"这一帧减一减到 0" —— 帧率一变,后者就不准了。
* ⚠ The test is "elapsed >= limit", not "this frame's step crossed zero" -- the latter breaks the
* moment the frame rate changes.
*/
static void tick(void) {
if (mode == 1 && running != 0 && elapsed() >= limit) {
acc = limit;
running = 0;
fired = 1;
chime();
print_str("[stopwatch] countdown reached zero\n");
}
}
/* 当前该显示的状态一行字 */
/* The one-line status the screen should show */
static char *status_text(void) {
if (mode == 0) {
if (running) return lang == 0 ? "秒表 · 计时中" : "stopwatch - running";
if (elapsed() > 0) return lang == 0 ? "秒表 · 已暂停" : "stopwatch - paused";
return lang == 0 ? "秒表 · 按开始" : "stopwatch - press start";
}
if (fired) return lang == 0 ? "时间到!" : "time is up!";
if (running) return lang == 0 ? "倒计时 · 计时中" : "countdown - running";
if (elapsed() > 0) return lang == 0 ? "倒计时 · 已暂停" : "countdown - paused";
return lang == 0 ? "倒计时 · 按开始" : "countdown - press start";
}
/* ── 版式与按钮几何(画与命中判定**只有一个真源**)──────────────────── */
/* -- Layout and button geometry (**one single source** for drawing and hit testing) -- */
/* 顶部第 i 个页签(共 2 个) */
/* The i-th tab at the top (there are two) */
static void tab_rect(int i, int *rx, int *ry, int *rw, int *rh) {
int w = (SW - pad * 2 - 8) / 2;
*rx = pad + i * (w + 8);
*ry = tab_y;
*rw = w;
*rh = tab_h;
}
/* 某一排里第 i 个按钮(共 n 个) */
/* The i-th button of `n` in one row */
static void btn_rect(int i, int n, int row_y, int h, int *rx, int *ry, int *rw, int *rh) {
int gap = 8;
int w = (SW - pad * 2 - gap * (n - 1)) / n;
*rx = pad + i * (w + gap);
*ry = row_y;
*rw = w;
*rh = h;
}
static void layout(void) {
int avail;
SW = ui_scr_get_w();
SH = ui_scr_get_h();
if (SW <= 0) SW = 360; /* 宿主答不上来时的兜底 */
/* fallback when the host cannot answer */
if (SH <= 0) SH = 620;
pad = SW / 20;
if (pad < 8) pad = 8;
fs = SW / 30;
if (fs < 12) fs = 12;
if (fs > 18) fs = 18;
avail = SW - pad * 2;
big_fs = avail / 5; /* 8 个字正好占满一行 */
/* eight digits fill one line exactly */
if (big_fs < 26) big_fs = 26;
if (big_fs > 64) big_fs = 64;
tab_y = pad;
tab_h = fs + 18;
big_y = tab_y + tab_h + 28 + big_fs;
status_y = big_y + fs + 18;
lap_top = status_y + 10;
pre_y = SH - pad - 200;
pre_h = fs + 20;
main_y = SH - pad - 148;
main_h = 52;
aux_y = SH - pad - 88;
aux_h = fs + 24;
hint_y = SH - pad - 18;
}
static void draw(void) {
int i;
int x;
int y;
int w;
int h;
int col;
int n;
int row_y;
int row_hh;
char tb[64];
char tb2[16];
tick(); /* ⚠ 先推进状态再画(倒计时到点就在这一句里定格)*/
/* ⚠ Advance the state before drawing (this is where the countdown stops) */
ui_2d_clear(C_BG);
/* 顶部两个页签:选中的那颗是蓝底深字
* The two tabs at the top: the selected one is a blue box with dark ink */
for (i = 0; i < 2; i++) {
tab_rect(i, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, mode == i ? C_TABON : C_CARD, 1, 0, 10);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100,
i == 0 ? (lang == 0 ? "秒表" : "Stopwatch")
: (lang == 0 ? "倒计时" : "Countdown"),
mode == i ? C_INK : C_DIM, fs, VML_ANCHOR_CENTER);
}
/* 大字读数:这一行就是整个程序的主角,颜色说明状态(绿=跑 / 琥珀=停 / 红=到点)
* The big readout, the star of the show: the colour carries the state (green running, amber
* paused, red finished) */
col = C_PAUSE;
if (running) col = C_RUN;
if (mode == 1 && fired) col = C_ALARM;
fmt_into(tb2, reading());
ui_2d_text_styled(SW / 2, big_y, tb2, col, big_fs, VML_ANCHOR_CENTER, VML_FONT_BOLD);
ui_2d_text(SW / 2, status_y, status_text(), C_DIM, fs, VML_ANCHOR_CENTER);
/* 秒表:把记下的圈列出来;倒计时:显示这次的上限,并说明怎么改
* Stopwatch: list the laps. Countdown: show the limit and how to change it. */
if (mode == 0) {
row_y = lap_top;
row_hh = fs + 14;
i = 0;
while (i < lap_n && row_y + row_hh < main_y - 6) {
tb[0] = 0;
strcat(tb, "L");
strcat(tb, numstr(i + 1));
strcat(tb, " ");
fmt_into(tb2, lap_ms[i]);
strcat(tb, tb2);
ui_2d_text(SW / 2, row_y + fs, tb, C_DIM, fs, VML_ANCHOR_CENTER);
row_y = row_y + row_hh;
i = i + 1;
}
if (lap_n == 0) {
ui_2d_text(SW / 2, lap_top + fs,
lang == 0 ? "计时中按 记圈 会记下这一圈" : "press Lap while running to record a lap",
C_DIM, fs - 2, VML_ANCHOR_CENTER);
}
} else {
tb[0] = 0;
strcat(tb, lang == 0 ? "上限 " : "limit ");
fmt_into(tb2, limit);
strcat(tb, tb2);
ui_2d_text(SW / 2, lap_top + fs, tb, C_DIM, fs, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, lap_top + fs + fs + 10,
lang == 0 ? "暂停时点一下可以 ±10 秒" : "while paused a tap shifts ten seconds",
C_DIM, fs - 2, VML_ANCHOR_CENTER);
}
/* 倒计时那一排预设(秒表模式下不画,也不参与命中判定)
* The preset row (countdown only; it is neither drawn nor hit-tested in stopwatch mode) */
if (mode == 1) {
i = 0;
while (i < 3) {
btn_rect(i, 3, pre_y, pre_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100,
i == 0 ? (lang == 0 ? "1 分" : "1 min")
: (i == 1 ? (lang == 0 ? "3 分" : "3 min")
: (lang == 0 ? "5 分" : "5 min")),
C_TITLE, fs, VML_ANCHOR_CENTER);
i = i + 1;
}
}
/* 中间那根大按钮:开始 / 暂停 / 重来
* The big button in the middle: start / pause / again */
btn_rect(0, 1, main_y, main_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, running ? C_PAUSE : C_TABON, 1, 0, 12);
ui_2d_text_styled(x + w / 2, y + h / 2 + (fs * 35) / 100,
running ? (lang == 0 ? "暂停" : "Pause")
: (fired ? (lang == 0 ? "重来" : "Again")
: (lang == 0 ? "开始" : "Start")),
C_INK, fs + 4, VML_ANCHOR_CENTER, VML_FONT_BOLD);
/* 下面一排:秒表 = 记圈 / 清零;倒计时 = −10 秒 / +10 秒 / 清零
* The row below: stopwatch gets lap and reset; the countdown gets -10 s, +10 s and reset */
if (mode == 0) {
n = 2;
btn_rect(0, 2, aux_y, aux_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100, lang == 0 ? "记圈" : "Lap",
C_TITLE, fs, VML_ANCHOR_CENTER);
btn_rect(1, 2, aux_y, aux_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100, lang == 0 ? "清零" : "Reset",
C_TITLE, fs, VML_ANCHOR_CENTER);
} else {
btn_rect(0, 3, aux_y, aux_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100, "-10s", C_TITLE, fs, VML_ANCHOR_CENTER);
btn_rect(1, 3, aux_y, aux_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100, "+10s", C_TITLE, fs, VML_ANCHOR_CENTER);
btn_rect(2, 3, aux_y, aux_h, &x, &y, &w, &h);
ui_2d_rect(x, y, w, h, C_BTN, 1, 0, 8);
ui_2d_text(x + w / 2, y + h / 2 + (fs * 35) / 100, lang == 0 ? "清零" : "Reset",
C_TITLE, fs, VML_ANCHOR_CENTER);
}
ui_2d_text(SW / 2, hint_y,
lang == 0 ? "空格 开始/暂停 · M 切模式 · L 记圈 · R 清零 · Esc 退出"
: "Space start/pause · M mode · L lap · R reset · Esc quit",
C_DIM, fs - 3, VML_ANCHOR_CENTER);
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* without this call you get a black screen */
}
/* 点/按落在哪儿:见上面那套 `*_rect`。返回 0 = 没落在任何按钮上。
* Where a tap landed, using the `*_rect` set above. Returns 0 when it hit nothing.
* 1/2 = 页签,3 = 大按钮,10..12 = 下面那一排,20..22 = 预设那一排。
* 1/2 are the tabs, 3 the big button, 10..12 the lower row and 20..22 the preset row.
*/
static int hit(int px, int py) {
int x;
int y;
int w;
int h;
int i;
for (i = 0; i < 2; i++) {
tab_rect(i, &x, &y, &w, &h);
if (px >= x && px < x + w && py >= y && py < y + h) return 1 + i;
}
btn_rect(0, 1, main_y, main_h, &x, &y, &w, &h);
if (px >= x && px < x + w && py >= y && py < y + h) return 3;
if (mode == 1) {
for (i = 0; i < 3; i++) {
btn_rect(i, 3, pre_y, pre_h, &x, &y, &w, &h);
if (px >= x && px < x + w && py >= y && py < y + h) return 20 + i;
}
for (i = 0; i < 3; i++) {
btn_rect(i, 3, aux_y, aux_h, &x, &y, &w, &h);
if (px >= x && px < x + w && py >= y && py < y + h) return 10 + i;
}
return 0;
}
for (i = 0; i < 2; i++) {
btn_rect(i, 2, aux_y, aux_h, &x, &y, &w, &h);
if (px >= x && px < x + w && py >= y && py < y + h) return 10 + i;
}
return 0;
}
/* 按钮编号 → 动作(触摸与键盘**走同一张表**,别一边改一边忘)
* Button id to action (touch and keyboard go through **the same table**, so one cannot drift) */
static void act(int id) {
if (id == 1) { set_mode(0); return; }
if (id == 2) { set_mode(1); return; }
if (id == 3) { start_stop(); return; }
if (mode == 0) {
if (id == 10) { lap(); return; }
if (id == 11) { reset_all(); print_str("[stopwatch] reset\n"); return; }
return;
}
if (id == 10) { adjust(0 - STEP_MS); return; }
if (id == 11) { adjust(STEP_MS); return; }
if (id == 12) { reset_all(); print_str("[stopwatch] reset\n"); return; }
if (id == 20) { preset(CD_1M); return; }
if (id == 21) { preset(CD_3M); return; }
if (id == 22) { preset(CD_5M); return; }
}
/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
int msg[4];
int t;
int t0idle;
int done;
int k;
int wait_ms;
int id;
lang = ui_scr_get_language(); /* 开局查一次,别每帧调 */
/* queried once, not every frame */
mode = 0;
limit = CD_3M; /* 倒计时的出厂值 = 3 分钟 */
/* the countdown's default: three minutes */
reset_all();
/* 开窗传 `0, 0`(= 尺寸交给宿主),**开窗之后**再取一次尺寸 —— 那一次拿到的才是**真实画布**,
* 窗口与画布才是一套坐标系(开窗前的 `layout()` 只是先算一版)。竖屏 + 不要手柄区。
* Open with `0, 0` (let the host size it) and take the size **again after** the open -- that pass is the
* **real canvas**, so window and canvas share one coordinate space (the pre-open `layout()` is just a first pass).
* Portrait, no gamepad. */
layout();
ui_win_open_ex("stopwatch.c", 0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout();
print_str("[stopwatch] ready (mode=stopwatch, limit=180000 ms)\n");
draw();
t0idle = ui_time_get_ms();
done = 0;
while (!done && ui_win_is_closed() == 0) {
/* 跑的时候把等待缩短,读数才跟得上(百分秒每 40 毫秒动一次)。
* Shorten the wait while running so hundredths keep up (they move every 40 ms).
* ⚠ 空闲计时**不因为"在跑"而重置** —— 否则这个程序永远不会自己退出(文件头 ⑤)。
* ⚠ The idle clock is **not** reset by "it is running", or the program would never end (pitfall 5). */
wait_ms = running ? 40 : 200;
t = ui_msg_wait(msg, wait_ms);
if (t == VML_MSG_NONE) {
if (running) { draw(); continue; } /* 在跑就重画这一帧 */
/* repaint while running */
if (ui_time_get_ms() - t0idle > IDLE_EXIT_MS) done = 1; /* 空闲超时 */
/* idle timeout */
continue;
}
t0idle = ui_time_get_ms();
if (t == VML_MSG_KEYDOWN) {
k = msg[1];
/* ⚠ 具名键**排在可打印字符之前**(文件头 ①):37/39 之类都紧挨着 32..126。
* ⚠ Named keys **come before printable characters** (pitfall 1): 37/39 sit right beside 32..126. */
if (k == VML_KEY_ESCAPE || k == 'q' || k == 'Q') {
done = 1;
} else if (k == VML_KEY_SPACE) {
act(3);
draw();
} else if (k == 'm' || k == 'M') {
set_mode(mode == 0 ? 1 : 0);
draw();
} else if (k == 'r' || k == 'R') {
reset_all();
print_str("[stopwatch] reset\n");
draw();
} else if (k == 'l' || k == 'L') {
lap();
draw();
} else if (k == '1' && mode == 1) {
preset(CD_1M);
draw();
} else if (k == '3' && mode == 1) {
preset(CD_3M);
draw();
} else if (k == '5' && mode == 1) {
preset(CD_5M);
draw();
} else if (k == VML_KEY_LEFT || k == '-') {
adjust(0 - STEP_MS);
draw();
} else if (k == VML_KEY_RIGHT || k == '+' || k == '=') {
adjust(STEP_MS);
draw();
}
continue;
}
if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
id = hit(msg[1], msg[2]);
if (id != 0) {
act(id);
draw();
}
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
layout();
draw();
continue;
}
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
/* 收尾只有一处:`Esc`/`q`、空闲超时、窗口返回键,三条路都从这里出去。
* One single exit: `Esc`/`q`, the idle timeout and the window's back arrow all leave here. */
print_str("[stopwatch] exit, reading ");
print_int(reading());
print_str(" ms, laps ");
print_int(lap_n);
print_str("\n");
return 0;
}