c/app/filedlg.c · 32884 字节 · 下载原文
/* filedlg.c —— 常用小程序⑨:**打开一个文件看它的头几行**(第 4 层:`ui_*` 图形界面)
* filedlg.c -- Everyday utility #9: **open a file and read its first lines** (layer 4: the `ui_*` interface)
*
* 干什么:一屏一个按钮。「选择文件」走 `ui_dlg_file_open`,拿到的是一个**沙箱相对路径**
* What it does: one button. "Choose a file" goes through `ui_dlg_file_open`, which hands back a
* (返回**字节数**,0 = 取消 / 没权限 / 这一端没有文件选择器),然后用 `fopen`/`fread` 把它读进一块
* **sandbox-relative path** (the return value is the **byte count**; 0 = cancelled / no permission /
* 扁平缓冲,显示**前若干行**与**文件大小**。
* this end has no file picker). The file is then read into one flat buffer with `fopen`/`fread`, and
* the first few lines plus the **file size** are shown.
*
* ## ⚠ 手机上才有 —— 桌面(vmlcli)上「没有文件选择器」才是**正确输出**
* ## ⚠ Phone-only -- on the desktop (vmlcli) "no file picker" is the **correct output**
*
* 桌面脚手架是**无头进程**:没有 GUI 可以弹选择框,`ui_dlg_file_open` 一律返回 0。
* The desktop scaffold is a **headless process**: there is no GUI to open a picker, so
* 而 **0 不是错误** —— 用户点了取消、没给权限、这一端没有该能力,三者都是 0,程序照常继续。
* `ui_dlg_file_open` always returns 0. That **0 is not an error**: a cancel, a missing permission and
* 所以降级分支要**明说**是"这一端没有",而不是留一块空白。
* a missing capability all look alike, and the program carries on. The degraded branch therefore
* **says** that this end lacks it instead of leaving a blank area.
*
* ## 桌面怎么看到"选了文件"那一半:宿主自带的输入脚本注入,**不用改一行源码**
* ## Seeing the picked-file branch on the desktop: the host's input-script injection, **no source change**
*
* printf '0 file calendar.c\n300 keydown 13\n360 keyup 13\n' > in.txt
* vmlcli filedlg.c --vml-home third_party/vml --input in.txt --timeout 40 --frame filedlg.png
*
* 第一条把"选择器返回的路径"喂给宿主(`CliVmlHost.InjectFile`),后两条**按一下按钮**(回车 13),
* The first line feeds the host the path a picker would return (`CliVmlHost.InjectFile`); the other two
* 于是那个文件真的被读出来、头几行真的被画在屏幕上 —— 换一个路径就是另一份内容。
* press the button (virtual key 13 = Enter), so the file really is read and its first lines really are
* drawn -- with another path you get another file.
*
* ## 踩到的坑(都是这一轮真撞上的,写在这里省得下一个人再撞)
* ## Pitfalls (all hit for real while writing this file; recorded so the next person does not)
*
* ① ⚠ **`fopen` 的第二个参数是 `int`(0 只读),不是 `"r"`** —— `Lib/shared/src/file.c` 的签名是
* ① ⚠ **`fopen`'s second argument is an `int` (0 = read), not `"r"`** -- the signature in
* `int fopen(const char *path, int mode)`,而 `stdio.h` 里声明的却是 `FILE* fopen(const char*,
* `Lib/shared/src/file.c` is `int fopen(const char *path, int mode)`, while `stdio.h` declares
* const char*)`。照 `stdio.h` 写 `fopen(p, "r")`,模式收到一个**指针值**,`#110` 回
* `FILE* fopen(const char*, const char*)`. Writing `fopen(p, "r")` per the header hands a **pointer**
* `-2`(`INVALID_PARAMETER`)**且一个字都不报**(实测)。所以本文件**不 include `stdio.h`**、
* to the mode and `#110` answers `-2` (`INVALID_PARAMETER`) **with no message at all** (measured).
* 自己按真签名声明 —— 顺带避开 `fgets`(那个函数在库里面**没有实现**,链上去会报未定义)。
* This file therefore declares the real signatures itself, which also avoids `fgets` (it has **no
* implementation** in the library and would fail to link).
* ② ✅ **文件大小现在直接用 `Lib/c/file.h` 的 `fsize(handle)`**(2026-10-07 起):那个包装原先
* ② ✅ **File size now uses `fsize(handle)` from `Lib/c/file.h`** (since 2026-10-07): the wrapper used
* 只是把 `#114` 原样转发、**没把命令号填进寄存器**(实测读到的是垃圾),现已修好 ——
* to forward `#114` without putting the command number in R1 (measured as garbage); it is fixed now —
* 判据见 `Examples/c/test/file_api.c`(逐项核对数值,且注入反证过)。
* criterion: `Examples/c/test/file_api.c` (value-by-value, with a counter-check by injection).
* ③ ⚠ **文件级标量别用负值初值**(本仓刚归档的 E187):`static int x = -1;` 读到的是 **0**。
* ③ ⚠ **Never give a file-scope scalar a negative initializer** (this repo's E187): `static int x = -1;`
* 所以下面所有哨兵量一律用**正数**(0 = 没有/未知,1 = 有),别拿 -1 当"还没有"。
* reads back as **0**. Every sentinel below is a **positive** number (0 = absent/unknown, 1 = present).
* ④ ⚠ **切行是"原地改字"**:`\n` 换成 `0` 之后,`buf` 同时是"整个文件"和"每行一个串",
* ④ ⚠ **Splitting lines rewrites the buffer in place**: after every `\n` becomes `0`, one buffer is
* 省掉一整张行表(本仓不许二维数组 / 指针数组)。代价是**切完就回不去了** ——
* both the whole file and a list of strings, with no line table at all (this repo forbids 2-D and
* `\n` 数一遍要放在切之前(见 `splitLines`)。
* pointer arrays). The price is that the split is destructive, so the line count must be taken
* **before** it (see `splitLines`).
* ⑤ ⚠ **"这一端没有"每拍都要重问一次**(与 `level.c` 坑 ② 同源):`--input` 的事件由另一个线程按
* ⑤ ⚠ **"This end has it" must be re-asked every tick** (same as pitfall 2 in `level.c`): the `--input`
* 时刻投递,程序可能在注入落地之前就点过按钮了。所以按钮**一直可点**,点一次问一次。
* events arrive from another thread on a timer, so the button may be pressed before the injection
* lands. The button therefore stays live and every press asks again.
* ⑥ ⚠ **裁行要裁"拷出来的那一份",绝不能就地写 0**(这一版第一稿就是这么错的,看图才发现):
* ⑥ ⚠ **Clip a copy, never write a `0` into the line itself** (the first draft did, and only the
* 太长的行就地截断之后,`lineAt` 从那个 0 往后走会拿到**上一行的尾巴** —— 屏幕上是"从第 2 行起
* picture showed it): once a long line is truncated in place, `lineAt` walks past that `0` and hands
* 每行都串位、后半截是一排空框"(那半截是 UTF-8 的残字节,一个字节画一个空框)。
* back the **tail of the previous line**, so from the second row on every line is shifted and its
* ⇒ 判据:**"行内容串位"+"空框"同时出现**,就是这条。
* second half is a row of empty boxes (each box is one leftover UTF-8 byte). The signature to look
* for is shifted content **and** empty boxes at the same time.
* ⑦ ⚠ **宽度按列数算,不按字符数**:一个汉字占 2 列(3 个字节),按"字符数"截会把 CJK 行切成
* ⑦ ⚠ **Width is counted in columns, not characters**: a CJK glyph is two columns (three bytes), so
* 两倍宽、右边顶出画布。`copyLine` 里按"首字节算 2、续字节算 0"数(见 `weather.c` 的 tty 80 列那条)。
* counting characters makes a CJK line twice as wide and it runs off the canvas. `copyLine` counts
* two for a lead byte and zero for a continuation (the same idea as the 80-column rule in `weather.c`).
* ⑧ ⚠ **截断不许把多字节字符切成半个字节**,而且后果比"最后一个字画错"严重得多:宿主读字符串时
* ⑧ ⚠ **Never cut a multibyte character in half**, and the damage is far worse than one wrong glyph:
* 先验 UTF-8(`VmlHostRuntime.Str`),**整条不合法就整条按 CP437 解** —— 于是**整行**变成乱码
* the host validates UTF-8 first (`VmlHostRuntime.Str`) and decodes the **whole** string as CP437
* (实测:末尾留了个光秃秃的 `E4`,`退出` 那行整行变成 `ΘÇÇσç║`,而它前面的 ASCII 一点事没有,
* when it is invalid, so the **entire line** turns to mojibake (measured: one bare trailing `E4`
* 看着像"这一行的中文坏了")。
* turned one Chinese word into mojibake while the ASCII around it stayed fine). `copyLine` therefore
* ⇒ `copyLine` 收尾时把末尾那个**不完整的多字节序列整个丢掉**(先回退续字节,再看首字节要求的
* gives back any **incomplete trailing sequence** (back off the continuations, then check how many
* 字节数够不够)。
* bytes the lead byte asked for).
* ⚠ 这个收尾**自己也能写错**(第一版就错了):判据必须是"**留下的**字节够不够"。
* ⚠ That give-back **has a bug of its own** (the first version had it): the test must ask whether the
* 拿"回退了几次"当字节数,会把一个**完整**的三字节字符砍成光秃秃的首字节,症状与前一条一模一样,
* **kept** bytes are enough. Using "how many times did I back off" instead chops a complete 3-byte
* 而原因正好相反(那次是**砍坏了一个好字符**)。所以两个量要分成两个变量:
* glyph down to a bare lead byte -- same symptom, opposite cause (a good character destroyed).
* `cut` 只用来**找**首字节,`tail` 才是"留下多少"(描述,不引原字,免得这行自己也算中文注释)。
* Two variables, then: `cut` only locates the lead byte, and `tail` is how much we keep.
*
* 版面:尺寸一律从宿主问(`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 must end with `ui_2d_present()`. The loop **must be bounded** (samples
* 写真正的程序时把 `IDLE_EXIT_MS` 去掉即可。
* have to be automatically verifiable): it exits after 10 s of idling, and `Esc`/`q` quits too.
*/
#include <vml_ui.h>
#include <stdlib.h> /* itoa --- digits to text via the library */
#include <file.h> /* fopen 家族的真声明(`int mode`)+ `fsize` —— 本文件不再自己抄一遍 */
#define IDLE_EXIT_MS 10000 /* 空闲 10 秒自己退出(验收用) */
/* idle exit after 10 s (for automated checks) */
#define PATH_CAP 200 /* 宿主写回来的路径缓冲区 */
/* the buffer the host writes the path into */
#define READ_CAP 4096 /* 一次读进多少字节(扁平缓冲,一次读够) */
/* how many bytes we read at once (one flat buffer) */
#define BTN_H 44
#define LINE_H 18 /* 正文行高(字号 13 配 18 不挤) */
/* body line height (font 13 fits comfortably in 18) */
/* ⚠ 这里原先**自己声明** `fopen` 家族(因为 `stdio.h` 的签名是错的、`file.h` 也对不上)。
* 2026-10-07 起 `Lib/c/file.h` 已与实现逐字对齐 ⇒ 直接 `#include <file.h>` 即可,
* 那份手抄的声明连同"不 include stdio.h 免得打架"的顾虑一起删掉。
* These declarations used to be hand-copied because both `stdio.h` and `file.h` disagreed with the
* implementation. Since 2026-10-07 `Lib/c/file.h` matches it verbatim, so `#include <file.h>` does. */
/* 配色(真彩 AA RRGGBB)*/
/* Colours (true colour AA RRGGBB) */
#define C_BG 0xFF0E1218
#define C_BOX 0xFF161B24
#define C_RING 0xFF3C4658
#define C_TITLE 0xFFE8E8F0
#define C_DIM 0xFF8A94A8
#define C_ERR 0xFFFF8A80 /* 没有这项能力 / 读不出来 */
/* capability absent, or unreadable */
#define C_NAME 0xFF6FC3FF /* 文件名 */
/* the file name */
#define C_SIZE 0xFFFFD166 /* 文件大小 */
/* the file size */
#define C_CODE 0xFFB8C4D8 /* 正文那几行 */
/* the body lines */
#define C_BTN 0xFF233046
#define C_BTNHOT 0xFF2F4060
/* ── 全局状态 ───────────────────────────────────────────────────────── */
/* -- Global state -- */
static int SW;
static int SH;
static int g_pad;
static int lang;
static char g_path[PATH_CAP]; /* 宿主写回来的路径(空串 = 还没选)*/
/* the path the host wrote back (empty = nothing chosen yet) */
static char g_buf[READ_CAP]; /* **唯一**的那块缓冲:先是整个文件,切完行就是每行一个串 */
/* the **one** buffer: the whole file first, then one string per line */
static int g_len; /* 真读进来多少字节 */
/* how many bytes were really read */
static int g_lines; /* 缓冲里有几行(切之前数的)*/
/* how many lines the buffer holds (counted before splitting) */
static int g_truncated; /* 1 = 文件比缓冲大,只读了一部分 */
/* 1 = the file is larger than the buffer, so only part was read */
static int g_size; /* 文件大小(字节;0 = 不知道)*/
/* the file size in bytes (0 = unknown) */
static int g_open; /* 1 = 手上有内容可画 */
/* 1 = we hold content to draw */
static int g_missing; /* 1 = 这一端**没有**文件选择器(上一次的结果)*/
/* 1 = this end **has no** file picker (last attempt's result) */
static int g_unreadable; /* 1 = 选了但读不出来 */
/* 1 = chosen but unreadable */
static int g_asked; /* 1 = 至少点过一次按钮 */
/* 1 = the button has been pressed at least once */
/* 版面(画与命中判定**必须用同一份**)*/
/* Layout (drawing and hit testing **must share this one copy**) */
static int boxX;
static int boxY;
static int boxW;
static int boxH;
static int btnX;
static int btnY;
static int btnW;
static int btnH;
static int infoY;
/* ── 系统调用:文件大小 ─────────────────────────────────────────────── */
/* -- Syscall: the file size -- */
/* ⚠ 这里原先手写了一次 `#114 FileControl(handle, 0, 0)`(取文件大小),因为当时库里那个
* `fsize` **没有把命令号填进寄存器**(实测读到垃圾,见文件头坑 ②)。
* 2026-10-07 那个包装已修好(`Lib/shared/src/file.c`,判据 `Examples/c/test/file_api.c`)
* ⇒ 这里改用 `#include <file.h>` 带来的 `fsize(handle)`。
* This used to hand-roll `#114 FileControl(handle, 0, 0)` because the library's `fsize` never put the
* command number into R1 (measuring as garbage). That wrapper was fixed on 2026-10-07, so `fsize` is used. */
/* ── 小工具 ─────────────────────────────────────────────────────────── */
/* -- Small helpers -- */
static char g_i10[16];
static char *numstr(int v) {
itoa(v, g_i10);
return g_i10;
}
static char g_base[64];
static char *baseName(void) {
int i;
int last;
int n;
last = -1;
i = 0;
while (g_path[i] != 0) {
if (g_path[i] == '/') last = i;
i = i + 1;
}
n = 0;
i = last + 1;
while (g_path[i] != 0 && n < 40) { g_base[n] = g_path[i]; i = i + 1; n = n + 1; }
g_base[n] = 0;
return g_base;
}
/* 原地切行:`\n` → `0`,顺手把 `\r` 也抹掉(CRLF 文件的每行末尾会多一个回车,
* Split lines in place: `\n` becomes `0` and `\r` is stripped too (a CRLF file leaves a carriage
* 留着的话行尾会多画一个看不见的方块)。返回实际切成几行。
* return at the end of every line, which renders as a stray box). Returns how many lines were cut.
*/
static int splitLines(char *s, int len) {
int i;
int n;
int start;
n = 0;
start = 0;
for (i = 0; i < len; i++) {
if (s[i] == '\n') {
if (i > start && s[i - 1] == '\r') s[i - 1] = 0;
s[i] = 0;
n = n + 1;
start = i + 1;
}
}
if (len > start) { s[len] = 0; n = n + 1; } /* 最后一行没有换行收尾 */
/* the last line has no trailing newline */
return n;
}
/* 第 n 行(切完之后用)—— 一维缓冲里"往前走 n 个 0"就是第 n 行,不需要行表。
* Line n (after splitting) -- walking past n zero bytes in the flat buffer is all it takes; no line table.
* 扫不到就返回空串(调用方按空行处理)。
* Returns an empty string when it runs out (the caller treats that as a blank line).
*/
static char *lineAt(char *s, int len, int n) {
int i;
int k;
i = 0;
k = 0;
while (k < n) {
while (i < len && s[i] != 0) i = i + 1;
if (i >= len) return s + len;
i = i + 1;
k = k + 1;
}
return s + i;
}
/* 把一行按**显示宽度**拷进 `g_line`(`maxCols` 是列数上限;半角 1 列、全角 2 列)。
* Copy one line into `g_line`, bounded by a **display width** (`maxCols`; half-width glyphs are one
* ⚠ **绝不就地裁剪**(裁的是 `g_buf` 里那一行本身)—— 见文件头坑 ⑥:就地写 0 会把"下一行的起点"
* column and full-width ones are two). **Never clip in place** (never touch the line inside `g_buf`):
* 一起切断,`lineAt` 从那个 0 往后走,拿到的是**上一行的尾巴**,屏幕上表现为"每行都串位、
* see pitfall 6 -- writing a `0` there also cuts off where the next line starts, so `lineAt` walks past
* 后半截还都是空框"(实测就是这一版)。
* it and hands back the **tail of the previous line**, which shows up as shifted lines whose second
* half is a row of empty boxes.
* ⚠ 回退过**续字节**(UTF-8 的 `10xxxxxx`):切在汉字中间会留半个字符,画出来是一个空框
* ⚠ Back off over **continuation bytes** (`10xxxxxx` in UTF-8): cutting inside a CJK glyph leaves half
* (一个汉字 3 个字节,第 2、3 个都带这个前缀,所以整个字必须吐回去)。
* a character, which renders as an empty box (a CJK glyph is 3 bytes and the 2nd and 3rd both carry
* that prefix, so the whole glyph is given back).
*/
static char g_line[160];
static void copyLine(char *s, int maxCols) {
int i;
int n;
int w;
int cw;
int cut;
int tail;
int need;
int b;
i = 0;
n = 0;
w = 0;
while (s[i] != 0 && n < 150) {
cw = 1;
if ((s[i] & 0x80) != 0) {
cw = ((s[i] & 0xC0) == 0xC0) ? 2 : 0; /* 多字节**首字节**算两列;续字节不另算 */
/* a multibyte **lead** byte is two columns; continuations add nothing */
}
if (cw > 0 && w + cw > maxCols) break;
g_line[n] = s[i];
i = i + 1;
n = n + 1;
w = w + cw;
}
/* ⚠ **收尾必须吐回"半个字"**(见坑 ⑧):宿主读字符串时先验 UTF-8,**不合法就整条按 CP437 解**
* ⚠ **The tail must give back a half-cut glyph** (pitfall 8): the host validates UTF-8 first and
* ⇒ 末尾留一个光秃秃的**首字节**(`E4` 后面没有续字节),不只是最后一个字画错,
* decodes the **whole string** as CP437 when it is invalid, so a lone **lead byte** at the end
* 而是**整行变成一串乱码**(实测那两个汉字变成四个拉丁字母加符号)。
* (`E4` with its continuations missing) does not just spoil the last glyph: the entire line turns
* into mojibake (measured: two Chinese glyphs became four Latin letters and symbols).
*/
tail = n;
cut = n; /* cut 只用来**找**最后一个字符的首字节,别拿它当"留下多少" */
/* cut only **locates** the last character's lead byte; it is not "how much we keep" */
while (cut > 0 && (g_line[cut - 1] & 0xC0) == 0x80) cut = cut - 1;
if (cut > 0) {
b = g_line[cut - 1] & 0xFF;
need = 1;
if (b >= 0xF0) need = 4;
else if (b >= 0xE0) need = 3;
else if (b >= 0xC0) need = 2;
/* ⚠ 判据是"**留下**的字节够不够"(`n - (cut - 1)` 才是这个字符的字节数)——
* ⚠ The test is whether **the kept bytes** are enough (`n - (cut - 1)` is the character's byte
* 写错成"cut 减了几次"会**把好字符砍掉**:实测 `E2 80 87`(三个字节都齐)被砍成单个 `E2`
* count); getting it wrong **chops a good character**: measured, a complete `E2 80 87` was cut
* ⇒ 整行退化成乱码(见坑 ⑧)。
* down to a bare `E2` and the whole line turned to mojibake (pitfall 8).
*/
if (n - (cut - 1) < need) tail = cut - 1;
}
g_line[tail] = 0;
}
/* ── 选文件并读进来 ─────────────────────────────────────────────────── */
/* -- Choosing a file and reading it in -- */
static void openFile(void) {
int n;
int h;
g_asked = 1;
g_open = 0;
g_missing = 0;
g_unreadable = 0;
g_len = 0;
g_lines = 0;
g_truncated = 0;
g_size = 0;
n = ui_dlg_file_open(g_path, PATH_CAP);
if (n <= 0) {
/* ⚠ **降级分支**:0 = 取消 / 没权限 / 这一端没有选择器,三者不区分(接口就是这么定的)。 */
/* ⚠ **The degraded branch**: 0 means cancelled / no permission / no picker here, and the API
* 界面上按"这一端没有"来说 —— 桌面上确实如此,手机上是用户自己取消。
* does not tell them apart. The UI words it as "this end lacks it", which is exactly the
* desktop's case and reads as a user cancel on the phone. */
g_missing = 1;
g_path[0] = 0;
return;
}
g_path[n] = 0;
h = fopen(g_path, 0);
if (h < 0) {
g_unreadable = 1;
return;
}
g_size = fsize(h); /* ⚠ 读之前先问大小(读完句柄位置就变了)*/
/* ⚠ Ask for the size before reading (reading moves the handle) */
g_len = fread(h, g_buf, READ_CAP - 1);
fclose(h);
if (g_len < 0) g_len = 0;
if (g_len >= READ_CAP - 1) g_truncated = 1;
g_buf[g_len] = 0;
/* ⚠ **切完就回不去了**:所以行数由 `splitLines` 自己数着返回(切之前数一遍也一样,
* ⚠ **The split is destructive**, so `splitLines` counts as it goes (counting beforehand would
* 只是别在切之后再去数一遍 —— 那时缓冲里已经没有 `\n` 了)。
* be equivalent; counting afterwards would find no `\n` left at all).
*/
g_lines = splitLines(g_buf, g_len);
g_open = 1;
}
/* ── 版面 ───────────────────────────────────────────────────────────── */
/* -- Layout -- */
static void layout(void) {
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;
btnH = BTN_H;
btnW = SW - g_pad * 2;
if (btnW > 280) btnW = 280;
btnX = (SW - btnW) / 2;
btnY = SH - g_pad - btnH;
infoY = btnY - 44; /* 按钮之上两行:文件名 / 大小 */
/* two lines above the button: name / size */
boxX = g_pad;
boxY = g_pad + 42;
boxW = SW - g_pad * 2;
boxH = infoY - boxY - 10;
if (boxH < 80) boxH = 80;
}
/* 正文区能放下几行(**画与"还有多少行没显示"用同一个数**)*/
/* How many body lines fit (the **same number** drives drawing and the "lines left over" note) */
static int maxBodyLines(void) {
int n;
n = (boxH - 20) / LINE_H;
if (n < 1) n = 1;
return n;
}
/* ── 绘制 ───────────────────────────────────────────────────────────── */
/* -- Drawing -- */
static void drawBody(void) {
int i;
int n;
int y;
int maxCols;
char *ln;
char buf[64];
if (g_open == 0) return;
maxCols = (boxW - 24) / 7; /* 字号 13:半角约 7px 一列 ⇒ 列数 = 可用宽 / 7 */
/* font 13: about 7 px per half-width column, so columns = width / 7 */
if (maxCols < 8) maxCols = 8;
n = maxBodyLines();
y = boxY + 12;
for (i = 0; i < n; i++) {
if (i >= g_lines) break;
ln = lineAt(g_buf, g_len, i);
copyLine(ln, maxCols); /* ⚠ **拷出来裁**,不碰 `g_buf` 里的那一行(见坑 ⑥)*/
/* ⚠ Clip **a copy**, never the line inside `g_buf` (pitfall 6) */
if (g_line[0] != 0) ui_2d_text(boxX + 12, y, g_line, C_CODE, 13, VML_ANCHOR_LEFT);
y = y + LINE_H;
}
if (g_lines > n) {
buf[0] = 0;
strcat(buf, lang == 0 ? "还有 " : "and ");
strcat(buf, numstr(g_lines - n));
strcat(buf, lang == 0 ? " 行没显示" : " more lines below");
ui_2d_text(boxX + 12, boxY + boxH - 20, buf, C_DIM, 11, VML_ANCHOR_LEFT);
}
}
static void drawEmpty(void) {
int cy;
cy = boxY + boxH / 2;
if (g_missing != 0) {
ui_2d_text(SW / 2, cy - 24, lang == 0 ? "没有选到文件" : "no file chosen",
C_ERR, 17, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, cy + 2, lang == 0 ? "(本机没有文件选择器;桌面上返回 0)"
: "(this device has no file picker; the desktop returns 0)",
C_ERR, 12, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, cy + 26, lang == 0 ? "0 不是错误:照常继续跑" : "0 is not an error: carry on",
C_DIM, 11, VML_ANCHOR_CENTER);
return;
}
if (g_unreadable != 0) {
ui_2d_text(SW / 2, cy - 18, lang == 0 ? "选了,但读不出来" : "chosen, but unreadable",
C_ERR, 17, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, cy + 8, lang == 0 ? "(文件不存在,或路径落在沙箱之外)"
: "(missing, or the path is outside the sandbox)",
C_DIM, 12, VML_ANCHOR_CENTER);
return;
}
ui_2d_text(SW / 2, cy - 18, lang == 0 ? "点下面的按钮选一个文件" : "press the button to choose a file",
C_DIM, 15, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, cy + 8, lang == 0 ? "会显示它的头几行与文件大小" : "its first lines and its size will show",
C_DIM, 12, VML_ANCHOR_CENTER);
}
static void drawInfo(void) {
char buf[96];
if (g_open == 0) return;
buf[0] = 0;
strcat(buf, lang == 0 ? "文件:" : "file: ");
strcat(buf, baseName());
ui_2d_text(SW / 2, infoY, buf, C_NAME, 13, VML_ANCHOR_CENTER);
buf[0] = 0;
if (g_truncated != 0) {
strcat(buf, numstr(g_len));
strcat(buf, lang == 0 ? " 字节(只读了开头这些)" : " bytes (only the first part was read)");
} else {
strcat(buf, numstr(g_size));
strcat(buf, lang == 0 ? " 字节 / " : " bytes / ");
strcat(buf, numstr(g_lines));
strcat(buf, lang == 0 ? " 行" : " lines");
}
ui_2d_text(SW / 2, infoY + 20, buf, C_SIZE, 12, VML_ANCHOR_CENTER);
}
static void draw(void) {
ui_2d_clear(C_BG);
ui_2d_text_styled(SW / 2, g_pad, lang == 0 ? "文件选择" : "File dialog",
C_TITLE, 18, VML_ANCHOR_CENTER, VML_FONT_BOLD);
if (g_open != 0) {
ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "fread 读进一块扁平缓冲,原地切行" : "one flat buffer, split in place",
C_DIM, 11, VML_ANCHOR_CENTER);
} else {
ui_2d_text(SW / 2, g_pad + 24, lang == 0 ? "手机上才有选择器;桌面上会如实报「没有」"
: "phones have the picker; the desktop reports \"absent\"",
C_DIM, 11, VML_ANCHOR_CENTER);
}
ui_2d_rect(boxX, boxY, boxW, boxH, C_BOX, 1, 0, 8);
ui_2d_rect(boxX, boxY, boxW, boxH, C_RING, 0, 1, 8);
if (g_open != 0) drawBody();
else drawEmpty();
drawInfo();
ui_2d_rect(btnX, btnY, btnW, btnH, C_BTN, 1, 0, btnH / 2);
ui_2d_rect(btnX, btnY, btnW, btnH, C_BTNHOT, 0, 1, btnH / 2);
ui_2d_text(SW / 2, btnY + (btnH - 15) / 2,
lang == 0 ? "选择文件 Enter" : "Choose a file Enter",
C_TITLE, 15, VML_ANCHOR_CENTER);
ui_2d_text(SW / 2, SH - 14, lang == 0 ? "回车 / 点按钮选中 Esc / q 退出"
: "Enter or tap to choose Esc / q quits",
C_DIM, 11, VML_ANCHOR_CENTER);
ui_2d_present(); /* ⚠ 不调这句 = 黑屏 */
/* without this call you get a black screen */
}
/* 点在选择按钮里吗(与 `draw` 用的是**同一组**坐标)*/
/* Did the tap land on the choose button (the **same** geometry `draw` uses) */
static int hitButton(int px, int py) {
if (px >= btnX && px < btnX + btnW && py >= btnY && py < btnY + btnH) return 1;
return 0;
}
/* ── 主流程 ─────────────────────────────────────────────────────────── */
/* -- Main flow -- */
int main(void) {
int msg[4];
int t;
int t0;
int done;
lang = ui_scr_get_language();
layout();
ui_win_open_ex(lang == 0 ? "文件选择" : "File dialog",
0, 0, VML_WIN_ROTATABLE, VML_WIN_NO_GAMEPAD);
layout(); /* 开窗之后画布尺寸才是准的,再算一次 */
/* the canvas size is only final after the window opens */
draw();
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;
}
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 if (k == VML_KEY_ENTER || k == VML_KEY_SPACE) { openFile(); draw(); }
continue;
}
if (t == VML_MSG_TOUCHDOWN || t == VML_MSG_MOUSEDOWN) {
if (hitButton(msg[1], msg[2]) != 0) { openFile(); draw(); }
continue;
}
if (t == VML_MSG_WINDOWORIENT || t == VML_MSG_WINDOWRESIZE) {
layout();
draw();
continue;
}
if (t == VML_MSG_WINDOWCLOSE) done = 1;
}
return 0;
}